merge mozilla-inbound to mozilla-central a=merge FIREFOX_AURORA_47_BASE
authorCarsten "Tomcat" Book <cbook@mozilla.com>
Mon, 07 Mar 2016 11:34:06 +0100
changeset 287009 68d3781deda0d4d58ec9877862830db89669b3a5
parent 286984 a75d87f4cf527815e6271932676800138c27da70 (current diff)
parent 287008 4b9a16cc88c2e2689dc2c9c47be8fc01ea5439da (diff)
child 287010 552ef9bae23386aa976f28e0d66ed2ae92bc2194
child 287017 087905ffec78fb4902e3f7dee90c3be2899ec0e7
child 287080 4ac661e25c72acd51c8c4fad4da64aa96e4fef11
push idunknown
push userunknown
push dateunknown
reviewersmerge
milestone47.0a1
merge mozilla-inbound to mozilla-central a=merge
layout/reftests/fonts/sil/Charis-license.txt
layout/reftests/fonts/sil/CharisSIL-R.ttf
--- a/dom/media/MediaRecorder.cpp
+++ b/dom/media/MediaRecorder.cpp
@@ -22,16 +22,18 @@
 #include "nsError.h"
 #include "nsIDocument.h"
 #include "nsIPermissionManager.h"
 #include "nsIPrincipal.h"
 #include "nsMimeTypes.h"
 #include "nsProxyRelease.h"
 #include "nsTArray.h"
 #include "GeckoProfiler.h"
+#include "nsContentTypeParser.h"
+#include "nsCharSeparatedTokenizer.h"
 
 #ifdef LOG
 #undef LOG
 #endif
 
 mozilla::LazyLogModule gMediaRecorderLog("MediaRecorder");
 #define LOG(type, msg) MOZ_LOG(gMediaRecorderLog, type, msg)
 
@@ -966,16 +968,21 @@ MediaRecorder::Constructor(const GlobalO
                            ErrorResult& aRv)
 {
   nsCOMPtr<nsPIDOMWindowInner> ownerWindow = do_QueryInterface(aGlobal.GetAsSupports());
   if (!ownerWindow) {
     aRv.Throw(NS_ERROR_FAILURE);
     return nullptr;
   }
 
+  if (!IsTypeSupported(aInitDict.mMimeType)) {
+    aRv.Throw(NS_ERROR_DOM_NOT_SUPPORTED_ERR);
+    return nullptr;
+  }
+
   RefPtr<MediaRecorder> object = new MediaRecorder(aStream, ownerWindow);
   object->SetOptions(aInitDict);
   return object.forget();
 }
 
 /* static */ already_AddRefed<MediaRecorder>
 MediaRecorder::Constructor(const GlobalObject& aGlobal,
                            AudioNode& aSrcAudioNode,
@@ -1000,16 +1007,21 @@ MediaRecorder::Constructor(const GlobalO
 
   // aSrcOutput doesn't matter to destination node because it has no output.
   if (aSrcAudioNode.NumberOfOutputs() > 0 &&
        aSrcOutput >= aSrcAudioNode.NumberOfOutputs()) {
     aRv.Throw(NS_ERROR_DOM_INDEX_SIZE_ERR);
     return nullptr;
   }
 
+  if (!IsTypeSupported(aInitDict.mMimeType)) {
+    aRv.Throw(NS_ERROR_DOM_NOT_SUPPORTED_ERR);
+    return nullptr;
+  }
+
   RefPtr<MediaRecorder> object = new MediaRecorder(aSrcAudioNode,
                                                      aSrcOutput,
                                                      ownerWindow);
   object->SetOptions(aInitDict);
   return object.forget();
 }
 
 void
@@ -1030,16 +1042,117 @@ MediaRecorder::SetOptions(const MediaRec
   //
   // Until dynamic changes are supported, I prefer to be safe and err
   // slightly high
   if (aInitDict.mBitsPerSecond.WasPassed() && !aInitDict.mVideoBitsPerSecond.WasPassed()) {
     mVideoBitsPerSecond = mBitsPerSecond;
   }
 }
 
+static char const *const gWebMAudioEncoderCodecs[3] = {
+  "vorbis",
+  "opus",
+  // no VP9 yet
+  nullptr,
+};
+static char const *const gWebMVideoEncoderCodecs[5] = {
+  "vorbis",
+  "opus",
+  "vp8",
+  "vp8.0",
+  // no VP9 yet
+  nullptr,
+};
+static char const *const gOggAudioEncoderCodecs[2] = {
+  "opus",
+  // we could support vorbis here too, but don't
+  nullptr,
+};
+
+template <class String>
+static bool
+CodecListContains(char const *const * aCodecs, const String& aCodec)
+{
+  for (int32_t i = 0; aCodecs[i]; ++i) {
+    if (aCodec.EqualsASCII(aCodecs[i]))
+      return true;
+  }
+  return false;
+}
+
+/* static */
+bool
+MediaRecorder::IsTypeSupported(GlobalObject& aGlobal, const nsAString& aMIMEType)
+{
+  return IsTypeSupported(aMIMEType);
+}
+
+/* static */
+bool
+MediaRecorder::IsTypeSupported(const nsAString& aMIMEType)
+{
+  char const* const* codeclist = nullptr;
+
+  if (aMIMEType.IsEmpty()) {
+    return true;
+  }
+
+  nsContentTypeParser parser(aMIMEType);
+  nsAutoString mimeType;
+  nsresult rv = parser.GetType(mimeType);
+  if (NS_FAILED(rv)) {
+    return false;
+  }
+
+  // effectively a 'switch (mimeType) {'
+  if (mimeType.EqualsLiteral(AUDIO_OGG)) {
+    if (MediaDecoder::IsOggEnabled() && MediaDecoder::IsOpusEnabled()) {
+      codeclist = gOggAudioEncoderCodecs;
+    }
+  }
+#ifdef MOZ_WEBM_ENCODER
+  else if (mimeType.EqualsLiteral(VIDEO_WEBM) &&
+           MediaEncoder::IsWebMEncoderEnabled()) {
+    codeclist = gWebMVideoEncoderCodecs;
+  }
+#endif
+#ifdef MOZ_OMX_ENCODER
+    // We're working on MP4 encoder support for desktop
+  else if (mimeType.EqualsLiteral(VIDEO_MP4) ||
+           mimeType.EqualsLiteral(AUDIO_3GPP) ||
+           mimeType.EqualsLiteral(AUDIO_3GPP2)) {
+    if (MediaEncoder::IsOMXEncoderEnabled()) {
+      // XXX check codecs for MP4/3GPP
+      return true;
+    }
+  }
+#endif
+
+  // codecs don't matter if we don't support the container
+  if (!codeclist) {
+    return false;
+  }
+  // now filter on codecs, and if needed rescind support
+  nsAutoString codecstring;
+  rv = parser.GetParameter("codecs", codecstring);
+
+  nsTArray<nsString> codecs;
+  if (!ParseCodecsString(codecstring, codecs)) {
+    return false;
+  }
+  for (const nsString& codec : codecs) {
+    if (!CodecListContains(codeclist, codec)) {
+      // Totally unsupported codec
+      return false;
+    }
+  }
+
+  return true;
+}
+
 nsresult
 MediaRecorder::CreateAndDispatchBlobEvent(already_AddRefed<nsIDOMBlob>&& aBlob)
 {
   MOZ_ASSERT(NS_IsMainThread(), "Not running on main thread");
   if (!CheckPrincipal()) {
     // Media is not same-origin, don't allow the data out.
     RefPtr<nsIDOMBlob> blob = aBlob;
     return NS_ERROR_DOM_SECURITY_ERR;
--- a/dom/media/MediaRecorder.h
+++ b/dom/media/MediaRecorder.h
@@ -17,16 +17,17 @@
 namespace mozilla {
 
 class AudioNodeStream;
 class DOMMediaStream;
 class ErrorResult;
 class MediaInputPort;
 struct MediaRecorderOptions;
 class MediaStream;
+class GlobalObject;
 
 namespace dom {
 
 class AudioNode;
 
 /**
  * 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,
@@ -73,16 +74,19 @@ public:
   void RequestData(ErrorResult& aResult);
   // Return the The DOMMediaStream passed from UA.
   DOMMediaStream* Stream() const { return mDOMStream; }
   // The current state of the MediaRecorder object.
   RecordingState State() const { return mState; }
   // Return the current encoding MIME type selected by the MediaEncoder.
   void GetMimeType(nsString &aMimeType);
 
+  static bool IsTypeSupported(GlobalObject& aGlobal, const nsAString& aType);
+  static bool IsTypeSupported(const nsAString& aType);
+
   // Construct a recorder with a DOM media stream object as its source.
   static already_AddRefed<MediaRecorder>
   Constructor(const GlobalObject& aGlobal,
               DOMMediaStream& aStream,
               const MediaRecorderOptions& aInitDict,
               ErrorResult& aRv);
   // Construct a recorder with a Web Audio destination node as its source.
   static already_AddRefed<MediaRecorder>
--- a/dom/media/MediaResource.cpp
+++ b/dom/media/MediaResource.cpp
@@ -1282,25 +1282,16 @@ nsresult FileMediaResource::Open(nsIStre
       NS_ENSURE_SUCCESS(rv, rv);
 
       rv = NS_NewLocalFileInputStream(
         getter_AddRefs(mInput), file, -1, -1, nsIFileInputStream::SHARE_DELETE);
     } else if (IsBlobURI(mURI)) {
       rv = NS_GetStreamForBlobURI(mURI, getter_AddRefs(mInput));
     }
   } else {
-
-#ifdef DEBUG
-    {
-      nsCOMPtr<nsILoadInfo> loadInfo = mChannel->GetLoadInfo();
-      MOZ_ASSERT((loadInfo->GetSecurityMode() &
-                 nsILoadInfo::SEC_REQUIRE_CORS_DATA_INHERITS) == 0,
-                 "can not enforce CORS when calling Open2()");
-    }
-#endif
     rv = mChannel->Open2(getter_AddRefs(mInput));
   }
   NS_ENSURE_SUCCESS(rv, rv);
 
   mSeekable = do_QueryInterface(mInput);
   if (!mSeekable) {
     // XXX The file may just be a .url or similar
     // shortcut that points to a Web site. We need to fix this by
@@ -1355,32 +1346,28 @@ already_AddRefed<MediaResource> FileMedi
   dom::HTMLMediaElement* element = owner->GetMediaElement();
   if (!element) {
     // The decoder is being shut down, so we can't clone
     return nullptr;
   }
   nsCOMPtr<nsILoadGroup> loadGroup = element->GetDocumentLoadGroup();
   NS_ENSURE_TRUE(loadGroup, nullptr);
 
-  nsSecurityFlags securityFlags = element->ShouldCheckAllowOrigin()
-                                  ? nsILoadInfo::SEC_REQUIRE_CORS_DATA_INHERITS
-                                  : nsILoadInfo::SEC_ALLOW_CROSS_ORIGIN_DATA_INHERITS;
-
   MOZ_ASSERT(element->IsAnyOfHTMLElements(nsGkAtoms::audio, nsGkAtoms::video));
   nsContentPolicyType contentPolicyType = element->IsHTMLElement(nsGkAtoms::audio) ?
     nsIContentPolicy::TYPE_INTERNAL_AUDIO : nsIContentPolicy::TYPE_INTERNAL_VIDEO;
 
   nsLoadFlags loadFlags = nsIRequest::LOAD_NORMAL | nsIChannel::LOAD_CLASSIFY_URI;
 
   nsCOMPtr<nsIChannel> channel;
   nsresult rv =
     NS_NewChannel(getter_AddRefs(channel),
                   mURI,
                   element,
-                  securityFlags,
+                  nsILoadInfo::SEC_ALLOW_CROSS_ORIGIN_DATA_INHERITS,
                   contentPolicyType,
                   loadGroup,
                   nullptr,  // aCallbacks
                   loadFlags);
 
   if (NS_FAILED(rv))
     return nullptr;
 
--- a/dom/media/test/mochitest.ini
+++ b/dom/media/test/mochitest.ini
@@ -727,16 +727,22 @@ tags=msg
 [test_mediarecorder_record_stopms.html]
 tags=msg
 [test_mediarecorder_record_timeslice.html]
 tags=msg capturestream
 [test_mediarecorder_reload_crash.html]
 tags=msg capturestream
 [test_mediarecorder_unsupported_src.html]
 tags=msg
+[test_mediarecorder_webm_support.html]
+skip-if = os == 'android' || arch == 'arm' || arch == 'arm64'
+tags=msg
+[test_mediarecorder_mp4_support.html]
+skip-if = toolkit != 'gonk' || android_version < '17' # Android/Gonk before SDK version 17 does not have the OMX Encoder API.
+tags=msg
 [test_mediarecorder_record_getdata_afterstart.html]
 tags=msg capturestream
 [test_mediatrack_consuming_mediaresource.html]
 [test_mediatrack_consuming_mediastream.html]
 tags=msg
 [test_mediatrack_events.html]
 skip-if = toolkit == 'gonk' && debug # bug 1065924
 [test_mediatrack_parsing_ogg.html]
new file mode 100644
--- /dev/null
+++ b/dom/media/test/test_mediarecorder_mp4_support.html
@@ -0,0 +1,16 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+  <title>Media Recording - test mp4 MIME support</title>
+  <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+ok(MediaRecorder.isTypeSupported('video/mp4'), 'Should support video/mp4');
+
+</script>
+</head>
+</html>
--- a/dom/media/test/test_mediarecorder_unsupported_src.html
+++ b/dom/media/test/test_mediarecorder_unsupported_src.html
@@ -5,17 +5,25 @@
   <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
   <script type="text/javascript" src="manifest.js"></script>
 </head>
 <body>
 <a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=957439">Mozilla Bug 957439</a>
 <pre id="test">
 <script class="testbody" type="text/javascript">
 
+
 function startTest() {
+  // also do general checks on mimetype support for audio-only
+  ok(MediaRecorder.isTypeSupported("audio/ogg"), 'Should support audio/ogg');
+  ok(MediaRecorder.isTypeSupported('audio/ogg; codecs="opus"'), 'Should support audio/ogg+opus');
+  ok(!MediaRecorder.isTypeSupported('audio/ogg; codecs="foobar"'), 'Should not support audio/ogg + unknown_codec');
+  ok(!MediaRecorder.isTypeSupported("video/webm"), 'Should not support video/webm');
+  ok(!MediaRecorder.isTypeSupported("video/mp4"), 'Should not support video/mp4');
+
   navigator.mozGetUserMedia({audio: false, video: true, fake: true},
     function(stream) {
 
       // Expected callback sequence should be:
       // 1. onerror (from start)
       // 2. onerror (from pause)
       // 3. ondataavailable
       // 4. onstop
new file mode 100644
--- /dev/null
+++ b/dom/media/test/test_mediarecorder_webm_support.html
@@ -0,0 +1,16 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+  <title>Media Recording - test WebM MIME support</title>
+  <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+ok(MediaRecorder.isTypeSupported('video/webm'), 'Should support video/webm');
+ok(MediaRecorder.isTypeSupported('video/webm; codecs="vp8, vorbis"'), 'Should support video/webm + vp8/vorbis');
+ok(!MediaRecorder.isTypeSupported('video/webm; codecs="vp9, vorbis"'), 'Should not support video/webm + vp9/vorbis');
+</script>
+</head>
+</html>
--- a/dom/media/webspeech/synth/cocoa/OSXSpeechSynthesizerService.mm
+++ b/dom/media/webspeech/synth/cocoa/OSXSpeechSynthesizerService.mm
@@ -146,16 +146,17 @@ SpeechTaskCallback::OnError(uint32_t aIn
   mTask->DispatchError(GetTimeDurationFromStart(), aIndex);
 }
 
 void
 SpeechTaskCallback::OnDidFinishSpeaking()
 {
   mTask->DispatchEnd(GetTimeDurationFromStart(), mCurrentIndex);
   // no longer needed
+  [mSpeechSynthesizer setDelegate:nil];
   mTask = nullptr;
 }
 
 @interface SpeechDelegate : NSObject<NSSpeechSynthesizerDelegate>
 {
 @private
   SpeechTaskCallback* mCallback;
 }
--- a/dom/webidl/MediaRecorder.webidl
+++ b/dom/webidl/MediaRecorder.webidl
@@ -42,16 +42,18 @@ interface MediaRecorder : EventTarget {
   [Throws]
   void pause();
 
   [Throws]
   void resume();
 
   [Throws]
   void requestData();
+
+  static boolean isTypeSupported(DOMString type);
 };
 
 dictionary MediaRecorderOptions {
   DOMString mimeType = ""; // Default encoding mimeType.
   unsigned long audioBitsPerSecond;
   unsigned long videoBitsPerSecond;
   unsigned long bitsPerSecond;
 };
--- a/image/SVGDocumentWrapper.cpp
+++ b/image/SVGDocumentWrapper.cpp
@@ -110,16 +110,22 @@ SVGDocumentWrapper::FlushImageTransformI
   svgElem->FlushImageTransformInvalidation();
   FlushLayout();
   mIgnoreInvalidation = false;
 }
 
 bool
 SVGDocumentWrapper::IsAnimated()
 {
+  // Can be called for animated images during shutdown, after we've
+  // already Observe()'d XPCOM shutdown and cleared out our mViewer pointer.
+  if (!mViewer) {
+    return false;
+  }
+
   nsIDocument* doc = mViewer->GetDocument();
   if (!doc) {
     return false;
   }
   if (doc->Timeline()->HasAnimations()) {
     // CSS animations (technically HasAnimations() also checks for CSS
     // transitions and Web animations but since SVG-as-an-image doesn't run
     // script they will never run in the document that we wrap).
new file mode 100644
--- /dev/null
+++ b/image/test/crashtests/1253362-1.html
@@ -0,0 +1,11 @@
+<!DOCTYPE html>
+<html>
+<head>
+<meta charset="UTF-8">
+</head>
+<body>
+
+<div style="content: url(data:image/svg+xml;charset=utf-8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20style%3D%22background%3A%20url%28data%3Aimage%2Fsvg%2Bxml%2C%253Csvg%2520xmlns%253D%2522http%253A%252F%252Fwww.w3.org%252F2000%252Fsvg%2522%253E%253C%252Fsvg%253E%29%22%3E%3C%2Fsvg%3E%0D%0A)"></div>
+
+</body>
+</html>
--- a/image/test/crashtests/crashtests.list
+++ b/image/test/crashtests/crashtests.list
@@ -16,16 +16,17 @@ load 1205923-1.html
 load 1212954-1.svg
 load 1235605.gif
 load 1241728-1.html
 load 1241729-1.html
 load 1242093-1.html
 load 1242778-1.png
 load 1249576-1.png
 load 1251091-1.html
+load 1253362-1.html
 load colormap-range.gif
 HTTP load delayedframe.sjs # A 3-frame animated GIF with an inordinate delay between the second and third frame
 
 # Animated gifs with a very large canvas, but tiny actual content.
 skip-if(B2G) load delaytest.html?523528-1.gif
 skip-if(B2G) load delaytest.html?523528-2.gif
 
 # Bug 1160801 - Ensure that we handle invalid disposal types.
--- a/ipc/glue/MessageChannel.cpp
+++ b/ipc/glue/MessageChannel.cpp
@@ -276,39 +276,219 @@ private:
     MessageChannel& mThat;
 
     // Disable harmful methods.
     CxxStackFrame() = delete;
     CxxStackFrame(const CxxStackFrame&) = delete;
     CxxStackFrame& operator=(const CxxStackFrame&) = delete;
 };
 
+class AutoEnterTransaction
+{
+public:
+    explicit AutoEnterTransaction(MessageChannel *aChan,
+                                  int32_t aMsgSeqno,
+                                  int32_t aTransactionID,
+                                  int aPriority)
+      : mChan(aChan),
+        mActive(true),
+        mOutgoing(true),
+        mPriority(aPriority),
+        mSeqno(aMsgSeqno),
+        mTransaction(aTransactionID),
+        mNext(mChan->mTransactionStack)
+    {
+        mChan->mMonitor->AssertCurrentThreadOwns();
+        mChan->mTransactionStack = this;
+    }
+
+    explicit AutoEnterTransaction(MessageChannel *aChan, const IPC::Message &aMessage)
+      : mChan(aChan),
+        mActive(true),
+        mOutgoing(false),
+        mPriority(aMessage.priority()),
+        mSeqno(aMessage.seqno()),
+        mTransaction(aMessage.transaction_id()),
+        mNext(mChan->mTransactionStack)
+    {
+        mChan->mMonitor->AssertCurrentThreadOwns();
+
+        if (!aMessage.is_sync()) {
+            mActive = false;
+            return;
+        }
+
+        mChan->mTransactionStack = this;
+    }
+
+    ~AutoEnterTransaction() {
+        mChan->mMonitor->AssertCurrentThreadOwns();
+        if (mActive) {
+            mChan->mTransactionStack = mNext;
+        }
+    }
+
+    void Cancel() {
+        AutoEnterTransaction *cur = mChan->mTransactionStack;
+        MOZ_RELEASE_ASSERT(cur == this);
+        while (cur && cur->mPriority != IPC::Message::PRIORITY_NORMAL) {
+            // Note that, in the following situation, we will cancel multiple
+            // transactions:
+            // 1. Parent sends high prio message P1 to child.
+            // 2. Child sends high prio message C1 to child.
+            // 3. Child dispatches P1, parent blocks.
+            // 4. Child cancels.
+            // In this case, both P1 and C1 are cancelled. The parent will
+            // remove C1 from its queue when it gets the cancellation message.
+            MOZ_RELEASE_ASSERT(cur->mActive);
+            cur->mActive = false;
+            cur = cur->mNext;
+        }
+
+        mChan->mTransactionStack = cur;
+
+        MOZ_RELEASE_ASSERT(IsComplete());
+    }
+
+    bool AwaitingSyncReply() const {
+        MOZ_RELEASE_ASSERT(mActive);
+        if (mOutgoing) {
+            return true;
+        }
+        return mNext ? mNext->AwaitingSyncReply() : false;
+    }
+
+    int AwaitingSyncReplyPriority() const {
+        MOZ_RELEASE_ASSERT(mActive);
+        if (mOutgoing) {
+            return mPriority;
+        }
+        return mNext ? mNext->AwaitingSyncReplyPriority() : 0;
+    }
+
+    bool DispatchingSyncMessage() const {
+        MOZ_RELEASE_ASSERT(mActive);
+        if (!mOutgoing) {
+            return true;
+        }
+        return mNext ? mNext->DispatchingSyncMessage() : false;
+    }
+
+    int DispatchingSyncMessagePriority() const {
+        MOZ_RELEASE_ASSERT(mActive);
+        if (!mOutgoing) {
+            return mPriority;
+        }
+        return mNext ? mNext->DispatchingSyncMessagePriority() : 0;
+    }
+
+    int Priority() const {
+        MOZ_RELEASE_ASSERT(mActive);
+        return mPriority;
+    }
+
+    int32_t SequenceNumber() const {
+        MOZ_RELEASE_ASSERT(mActive);
+        return mSeqno;
+    }
+
+    int32_t TransactionID() const {
+        MOZ_RELEASE_ASSERT(mActive);
+        return mTransaction;
+    }
+
+    void ReceivedReply(const IPC::Message& aMessage) {
+        MOZ_RELEASE_ASSERT(aMessage.seqno() == mSeqno);
+        MOZ_RELEASE_ASSERT(aMessage.transaction_id() == mTransaction);
+        MOZ_RELEASE_ASSERT(!mReply);
+        IPC_LOG("Reply received on worker thread: seqno=%d", mSeqno);
+        mReply = new IPC::Message(aMessage);
+        MOZ_RELEASE_ASSERT(IsComplete());
+    }
+
+    void HandleReply(const IPC::Message& aMessage) {
+        AutoEnterTransaction *cur = mChan->mTransactionStack;
+        MOZ_RELEASE_ASSERT(cur == this);
+        while (cur) {
+            MOZ_RELEASE_ASSERT(cur->mActive);
+            if (aMessage.seqno() == cur->mSeqno) {
+                cur->ReceivedReply(aMessage);
+                break;
+            }
+            cur = cur->mNext;
+            MOZ_RELEASE_ASSERT(cur);
+        }
+    }
+
+    bool IsComplete() {
+        return !mActive || mReply;
+    }
+
+    bool IsOutgoing() {
+        return mOutgoing;
+    }
+
+    bool IsCanceled() {
+        return !mActive;
+    }
+
+    bool IsBottom() const {
+        return !mNext;
+    }
+
+    bool IsError() {
+        MOZ_RELEASE_ASSERT(mReply);
+        return mReply->is_reply_error();
+    }
+
+    nsAutoPtr<IPC::Message> GetReply() {
+        return Move(mReply);
+    }
+
+private:
+    MessageChannel *mChan;
+
+    // Active is true if this transaction is on the mChan->mTransactionStack
+    // stack. Generally we're not on the stack if the transaction was canceled
+    // or if it was for a message that doesn't require transactions (an async
+    // message).
+    bool mActive;
+
+    // Is this stack frame for an outgoing message?
+    bool mOutgoing;
+
+    // Properties of the message being sent/received.
+    int mPriority;
+    int32_t mSeqno;
+    int32_t mTransaction;
+
+    // Next item in mChan->mTransactionStack.
+    AutoEnterTransaction *mNext;
+
+    // Pointer the a reply received for this message, if one was received.
+    nsAutoPtr<IPC::Message> mReply;
+};
+
 MessageChannel::MessageChannel(MessageListener *aListener)
   : mListener(aListener),
     mChannelState(ChannelClosed),
     mSide(UnknownSide),
     mLink(nullptr),
     mWorkerLoop(nullptr),
     mChannelErrorTask(nullptr),
     mWorkerLoopID(-1),
     mTimeoutMs(kNoTimeout),
     mInTimeoutSecondHalf(false),
     mNextSeqno(0),
     mLastSendError(SyncSendError::SendSuccess),
-    mAwaitingSyncReply(false),
-    mAwaitingSyncReplyPriority(0),
-    mDispatchingSyncMessage(false),
-    mDispatchingSyncMessagePriority(0),
     mDispatchingAsyncMessage(false),
     mDispatchingAsyncMessagePriority(0),
-    mCurrentTransaction(0),
-    mPendingSendPriorities(0),
+    mTransactionStack(nullptr),
     mTimedOutMessageSeqno(0),
     mTimedOutMessagePriority(0),
-    mRecvdErrors(0),
     mRemoteStackDepthGuess(false),
     mSawInterruptOutMsg(false),
     mIsWaitingForIncoming(false),
     mAbortOnError(false),
     mFlags(REQUIRE_DEFAULT),
     mPeerPidSet(false),
     mPeerPid(-1)
 {
@@ -339,16 +519,61 @@ MessageChannel::~MessageChannel()
     IPC_ASSERT(mCxxStackFrames.empty(), "mismatched CxxStackFrame ctor/dtors");
 #ifdef OS_WIN
     BOOL ok = CloseHandle(mEvent);
     MOZ_RELEASE_ASSERT(ok);
 #endif
     Clear();
 }
 
+// This function returns the current transaction ID. Since the notion of a
+// "current transaction" can be hard to define when messages race with each
+// other and one gets canceled and the other doesn't, we require that this
+// function is only called when the current transaction is known to be for a
+// high priority message. In that case, we know for sure what the caller is
+// looking for.
+int32_t
+MessageChannel::CurrentHighPriorityTransaction() const
+{
+    mMonitor->AssertCurrentThreadOwns();
+    if (!mTransactionStack) {
+        return 0;
+    }
+    MOZ_RELEASE_ASSERT(mTransactionStack->Priority() == IPC::Message::PRIORITY_HIGH);
+    return mTransactionStack->TransactionID();
+}
+
+bool
+MessageChannel::AwaitingSyncReply() const
+{
+    mMonitor->AssertCurrentThreadOwns();
+    return mTransactionStack ? mTransactionStack->AwaitingSyncReply() : false;
+}
+
+int
+MessageChannel::AwaitingSyncReplyPriority() const
+{
+    mMonitor->AssertCurrentThreadOwns();
+    return mTransactionStack ? mTransactionStack->AwaitingSyncReplyPriority() : 0;
+}
+
+bool
+MessageChannel::DispatchingSyncMessage() const
+{
+    mMonitor->AssertCurrentThreadOwns();
+    return mTransactionStack ? mTransactionStack->DispatchingSyncMessage() : false;
+}
+
+int
+MessageChannel::DispatchingSyncMessagePriority() const
+{
+    mMonitor->AssertCurrentThreadOwns();
+    return mTransactionStack ? mTransactionStack->DispatchingSyncMessagePriority() : 0;
+}
+
 static void
 PrintErrorMessage(Side side, const char* channelName, const char* msg)
 {
     const char *from = (side == ChildSide)
                        ? "Child"
                        : ((side == ParentSide) ? "Parent" : "Unknown");
     printf_stderr("\n###!!! [%s][%s] Error: %s\n\n", from, channelName, msg);
 }
@@ -400,17 +625,16 @@ MessageChannel::Clear()
 
     if (mChannelErrorTask) {
         mChannelErrorTask->Cancel();
         mChannelErrorTask = nullptr;
     }
 
     // Free up any memory used by pending messages.
     mPending.clear();
-    mRecvd = nullptr;
     mOutOfTurnReplies.clear();
     while (!mDeferred.empty()) {
         mDeferred.pop();
     }
 }
 
 bool
 MessageChannel::Open(Transport* aTransport, MessageLoop* aIOLoop, Side aSide)
@@ -617,17 +841,17 @@ MessageChannel::ShouldDeferMessage(const
     // race by dispatching in the child and deferring the incoming message in
     // the parent. However, the parent still needs to dispatch nested sync
     // messages.
     //
     // Deferring in the parent only sort of breaks message ordering. When the
     // child's message comes in, we can pretend the child hasn't quite
     // finished sending it yet. Since the message is sync, we know that the
     // child hasn't moved on yet.
-    return mSide == ParentSide && aMsg.transaction_id() != mCurrentTransaction;
+    return mSide == ParentSide && aMsg.transaction_id() != CurrentHighPriorityTransaction();
 }
 
 // Predicate that is true for messages that should be consolidated if 'compress' is set.
 class MatchingKinds {
     typedef IPC::Message Message;
     Message::msgid_t mType;
     int32_t mRoutingId;
 public:
@@ -654,43 +878,27 @@ MessageChannel::OnMessageReceivedFromLin
 
         if (aMsg.seqno() == mTimedOutMessageSeqno) {
             // Drop the message, but allow future sync messages to be sent.
             IPC_LOG("Received reply to timedout message; igoring; xid=%d", mTimedOutMessageSeqno);
             EndTimeout();
             return;
         }
 
-        MOZ_RELEASE_ASSERT(aMsg.transaction_id() == mCurrentTransaction);
         MOZ_RELEASE_ASSERT(AwaitingSyncReply());
-        MOZ_RELEASE_ASSERT(!mRecvd);
         MOZ_RELEASE_ASSERT(!mTimedOutMessageSeqno);
 
-        // Rather than storing errors in mRecvd, we mark them in
-        // mRecvdErrors. We need a counter because multiple replies can arrive
-        // when a timeout happens, as in the following example. Imagine the
-        // child is running slowly. The parent sends a sync message P1. It times
-        // out. The child eventually sends a sync message C1. While waiting for
-        // the C1 response, the child dispatches P1. In doing so, it sends sync
-        // message C2. At that point, it's valid for the parent to send error
-        // responses for both C1 and C2.
-        if (aMsg.is_reply_error()) {
-            mRecvdErrors++;
-            NotifyWorkerThread();
-            return;
-        }
-
-        mRecvd = new Message(aMsg);
+        mTransactionStack->HandleReply(aMsg);
         NotifyWorkerThread();
         return;
     }
 
     // Prioritized messages cannot be compressed.
     MOZ_RELEASE_ASSERT(aMsg.compress_type() == IPC::Message::COMPRESSION_NONE ||
-                          aMsg.priority() == IPC::Message::PRIORITY_NORMAL);
+                       aMsg.priority() == IPC::Message::PRIORITY_NORMAL);
 
     bool compress = false;
     if (aMsg.compress_type() == IPC::Message::COMPRESSION_ENABLED) {
         compress = (!mPending.empty() &&
                     mPending.back().type() == aMsg.type() &&
                     mPending.back().routing_id() == aMsg.routing_id());
         if (compress) {
             // This message type has compression enabled, and the back of the
@@ -763,87 +971,71 @@ MessageChannel::OnMessageReceivedFromLin
             // If we compressed away the previous message, we'll re-use
             // its pending task.
             mWorkerLoop->PostTask(FROM_HERE, new DequeueTask(mDequeueOneTask));
         }
     }
 }
 
 void
-MessageChannel::ProcessPendingRequests(int seqno, int transaction)
+MessageChannel::ProcessPendingRequests(AutoEnterTransaction& aTransaction)
 {
+    int32_t seqno = aTransaction.SequenceNumber();
+    int32_t transaction = aTransaction.TransactionID();
+
     IPC_LOG("ProcessPendingRequests for seqno=%d, xid=%d", seqno, transaction);
 
     // Loop until there aren't any more priority messages to process.
     for (;;) {
         // If we canceled during ProcessPendingRequest, then we need to leave
         // immediately because the results of ShouldDeferMessage will be
         // operating with weird state (as if no Send is in progress). That could
         // cause even normal priority sync messages to be processed (but not
         // normal priority async messages), which would break message ordering.
-        if (WasTransactionCanceled(transaction)) {
+        if (aTransaction.IsCanceled()) {
             return;
         }
 
         mozilla::Vector<Message> toProcess;
 
         for (MessageQueue::iterator it = mPending.begin(); it != mPending.end(); ) {
             Message &msg = *it;
 
-            MOZ_RELEASE_ASSERT(mCurrentTransaction == transaction,
-                                  "Calling ShouldDeferMessage when cancelled");
+            MOZ_RELEASE_ASSERT(!aTransaction.IsCanceled(),
+                               "Calling ShouldDeferMessage when cancelled");
             bool defer = ShouldDeferMessage(msg);
 
             // Only log the interesting messages.
             if (msg.is_sync() || msg.priority() == IPC::Message::PRIORITY_URGENT) {
                 IPC_LOG("ShouldDeferMessage(seqno=%d) = %d", msg.seqno(), defer);
             }
 
             if (!defer) {
                 if (!toProcess.append(Move(msg)))
                     MOZ_CRASH();
                 it = mPending.erase(it);
                 continue;
             }
             it++;
         }
 
-        if (toProcess.empty())
+        if (toProcess.empty()) {
             break;
+        }
 
         // Processing these messages could result in more messages, so we
         // loop around to check for more afterwards.
 
-        for (auto it = toProcess.begin(); it != toProcess.end(); it++)
+        for (auto it = toProcess.begin(); it != toProcess.end(); it++) {
             ProcessPendingRequest(*it);
+        }
     }
 }
 
 bool
-MessageChannel::WasTransactionCanceled(int transaction)
-{
-    if (transaction != mCurrentTransaction) {
-        // Imagine this scenario:
-        // 1. Child sends high prio sync message H1.
-        // 2. Parent sends reply to H1.
-        // 3. Parent sends high prio sync message H2.
-        // 4. Child's link thread receives H1 reply and H2 before worker wakes up.
-        // 5. Child dispatches H2 while still waiting for H1 reply.
-        // 6. Child cancels H2.
-        //
-        // In this case H1 will also be considered cancelled. However, its
-        // reply is still sitting in mRecvd, which can trip up later Sends. So
-        // we null it out here.
-        mRecvd = nullptr;
-        return true;
-    }
-    return false;
-}
-
-bool
 MessageChannel::Send(Message* aMsg, Message* aReply)
 {
     nsAutoPtr<Message> msg(aMsg);
 
     // Sanity checks.
     AssertWorkerThread();
     mMonitor->AssertNotCurrentThreadOwns();
 
@@ -861,180 +1053,173 @@ MessageChannel::Send(Message* aMsg, Mess
         // and we haven't received a reply for it. Once the original timed-out
         // message receives a reply, we'll be able to send more sync messages
         // again.
         IPC_LOG("Send() failed due to previous timeout");
         mLastSendError = SyncSendError::PreviousTimeout;
         return false;
     }
 
-    if (mCurrentTransaction &&
-        DispatchingSyncMessagePriority() == IPC::Message::PRIORITY_NORMAL &&
+    if (DispatchingSyncMessagePriority() == IPC::Message::PRIORITY_NORMAL &&
         msg->priority() > IPC::Message::PRIORITY_NORMAL)
     {
         // Don't allow sending CPOWs while we're dispatching a sync message.
         // If you want to do that, use sendRpcMessage instead.
         IPC_LOG("Prio forbids send");
         mLastSendError = SyncSendError::SendingCPOWWhileDispatchingSync;
         return false;
     }
 
-    if (mCurrentTransaction &&
-        (DispatchingSyncMessagePriority() == IPC::Message::PRIORITY_URGENT ||
-         DispatchingAsyncMessagePriority() == IPC::Message::PRIORITY_URGENT))
+    if (DispatchingSyncMessagePriority() == IPC::Message::PRIORITY_URGENT ||
+        DispatchingAsyncMessagePriority() == IPC::Message::PRIORITY_URGENT)
     {
         // Generally only the parent dispatches urgent messages. And the only
         // sync messages it can send are high-priority. Mainly we want to ensure
         // here that we don't return false for non-CPOW messages.
         MOZ_RELEASE_ASSERT(msg->priority() == IPC::Message::PRIORITY_HIGH);
         IPC_LOG("Sending while dispatching urgent message");
         mLastSendError = SyncSendError::SendingCPOWWhileDispatchingUrgent;
         return false;
     }
 
-    if (mCurrentTransaction &&
-        (msg->priority() < DispatchingSyncMessagePriority() ||
-         msg->priority() < AwaitingSyncReplyPriority()))
+    if (msg->priority() < DispatchingSyncMessagePriority() ||
+        msg->priority() < AwaitingSyncReplyPriority())
     {
         MOZ_RELEASE_ASSERT(DispatchingSyncMessage() || DispatchingAsyncMessage());
         IPC_LOG("Cancel from Send");
-        CancelMessage *cancel = new CancelMessage(mCurrentTransaction);
-        CancelTransaction(mCurrentTransaction);
+        CancelMessage *cancel = new CancelMessage(CurrentHighPriorityTransaction());
+        CancelTransaction(CurrentHighPriorityTransaction());
         mLink->SendMessage(cancel);
     }
 
     IPC_ASSERT(msg->is_sync(), "can only Send() sync messages here");
 
-    if (mCurrentTransaction) {
-        IPC_ASSERT(msg->priority() >= DispatchingSyncMessagePriority(),
-                   "can't send sync message of a lesser priority than what's being dispatched");
-        IPC_ASSERT(AwaitingSyncReplyPriority() <= msg->priority(),
-                   "nested sync message sends must be of increasing priority");
-        IPC_ASSERT(DispatchingSyncMessagePriority() != IPC::Message::PRIORITY_URGENT,
-                   "not allowed to send messages while dispatching urgent messages");
-    }
+    IPC_ASSERT(msg->priority() >= DispatchingSyncMessagePriority(),
+               "can't send sync message of a lesser priority than what's being dispatched");
+    IPC_ASSERT(AwaitingSyncReplyPriority() <= msg->priority(),
+               "nested sync message sends must be of increasing priority");
+    IPC_ASSERT(DispatchingSyncMessagePriority() != IPC::Message::PRIORITY_URGENT,
+               "not allowed to send messages while dispatching urgent messages");
 
     IPC_ASSERT(DispatchingAsyncMessagePriority() != IPC::Message::PRIORITY_URGENT,
                "not allowed to send messages while dispatching urgent messages");
 
     if (!Connected()) {
         ReportConnectionError("MessageChannel::SendAndWait", msg);
         mLastSendError = SyncSendError::NotConnectedBeforeSend;
         return false;
     }
 
     msg->set_seqno(NextSeqno());
 
     int32_t seqno = msg->seqno();
     int prio = msg->priority();
     msgid_t replyType = msg->type() + 1;
 
-    AutoSetValue<bool> replies(mAwaitingSyncReply, true);
-    AutoSetValue<int> prioSet(mAwaitingSyncReplyPriority, prio);
-    AutoEnterTransaction transact(this, seqno);
+    AutoEnterTransaction *stackTop = mTransactionStack;
 
-    int prios = mPendingSendPriorities | (1 << prio);
-    AutoSetValue<int> priosSet(mPendingSendPriorities, prios);
-
-    int32_t transaction = mCurrentTransaction;
+    // If the most recent message on the stack is high priority, then our
+    // message should nest inside that and we use the same transaction
+    // ID. Otherwise we need a new transaction ID (so we use the seqno of the
+    // message we're sending).
+    bool nest = stackTop && stackTop->Priority() == IPC::Message::PRIORITY_HIGH;
+    int32_t transaction = nest ? stackTop->TransactionID() : seqno;
     msg->set_transaction_id(transaction);
 
-    IPC_LOG("Send seqno=%d, xid=%d, pending=%d", seqno, transaction, prios);
+    bool handleWindowsMessages = mListener->HandleWindowsMessages(*aMsg);
+    AutoEnterTransaction transact(this, seqno, transaction, prio);
 
-    bool handleWindowsMessages = mListener->HandleWindowsMessages(*aMsg);
+    IPC_LOG("Send seqno=%d, xid=%d", seqno, transaction);
+
     mLink->SendMessage(msg.forget());
 
     while (true) {
-        ProcessPendingRequests(seqno, transaction);
-        if (WasTransactionCanceled(transaction)) {
-            IPC_LOG("Other side canceled seqno=%d, xid=%d", seqno, transaction);
-            mLastSendError = SyncSendError::CancelledAfterSend;
-            return false;
+        MOZ_RELEASE_ASSERT(!transact.IsCanceled());
+        ProcessPendingRequests(transact);
+        if (transact.IsComplete()) {
+            break;
         }
         if (!Connected()) {
             ReportConnectionError("MessageChannel::Send");
             mLastSendError = SyncSendError::DisconnectedDuringSend;
             return false;
         }
 
-        // See if we've received a reply.
-        if (mRecvdErrors) {
-            IPC_LOG("Error: seqno=%d, xid=%d", seqno, transaction);
-            mRecvdErrors--;
-            mLastSendError = SyncSendError::ReplyError;
-            return false;
-        }
+        MOZ_RELEASE_ASSERT(!mTimedOutMessageSeqno);
+        MOZ_RELEASE_ASSERT(!transact.IsComplete());
+        MOZ_RELEASE_ASSERT(mTransactionStack == &transact);
 
-        if (mRecvd) {
-            IPC_LOG("Got reply: seqno=%d, xid=%d", seqno, transaction);
-            break;
-        }
+        bool maybeTimedOut = !WaitForSyncNotify(handleWindowsMessages);
 
-        MOZ_RELEASE_ASSERT(!mTimedOutMessageSeqno);
-
-        MOZ_RELEASE_ASSERT(mCurrentTransaction == transaction);
-        bool maybeTimedOut = !WaitForSyncNotify(handleWindowsMessages);
         if (mListener->NeedArtificialSleep()) {
             MonitorAutoUnlock unlock(*mMonitor);
             mListener->ArtificialSleep();
         }
 
         if (!Connected()) {
             ReportConnectionError("MessageChannel::SendAndWait");
             mLastSendError = SyncSendError::DisconnectedDuringSend;
             return false;
         }
 
-        if (WasTransactionCanceled(transaction)) {
-            IPC_LOG("Other side canceled seqno=%d, xid=%d", seqno, transaction);
-            mLastSendError = SyncSendError::CancelledAfterSend;
-            return false;
+        if (transact.IsCanceled()) {
+            break;
         }
 
+        MOZ_RELEASE_ASSERT(mTransactionStack == &transact);
+
         // We only time out a message if it initiated a new transaction (i.e.,
         // if neither side has any other message Sends on the stack).
-        bool canTimeOut = transaction == seqno;
+        bool canTimeOut = transact.IsBottom();
         if (maybeTimedOut && canTimeOut && !ShouldContinueFromTimeout()) {
             // Since ShouldContinueFromTimeout drops the lock, we need to
             // re-check all our conditions here. We shouldn't time out if any of
             // these things happen because there won't be a reply to the timed
             // out message in these cases.
-            if (WasTransactionCanceled(transaction)) {
-                IPC_LOG("Other side canceled seqno=%d, xid=%d", seqno, transaction);
-                mLastSendError = SyncSendError::CancelledAfterSend;
-                return false;
-            }
-            if (mRecvdErrors) {
-                mRecvdErrors--;
-                mLastSendError = SyncSendError::ReplyError;
-                return false;
-            }
-            if (mRecvd) {
+            if (transact.IsComplete()) {
                 break;
             }
 
             IPC_LOG("Timing out Send: xid=%d", transaction);
 
             mTimedOutMessageSeqno = seqno;
             mTimedOutMessagePriority = prio;
             mLastSendError = SyncSendError::TimedOut;
             return false;
         }
+
+        if (transact.IsCanceled()) {
+            break;
+        }
+    }
+
+    if (transact.IsCanceled()) {
+        IPC_LOG("Other side canceled seqno=%d, xid=%d", seqno, transaction);
+        mLastSendError = SyncSendError::CancelledAfterSend;
+        return false;
     }
 
-    MOZ_RELEASE_ASSERT(mRecvd);
-    MOZ_RELEASE_ASSERT(mRecvd->is_reply(), "expected reply");
-    MOZ_RELEASE_ASSERT(!mRecvd->is_reply_error());
-    MOZ_RELEASE_ASSERT(mRecvd->seqno() == seqno);
-    MOZ_RELEASE_ASSERT(mRecvd->type() == replyType, "wrong reply type");
-    MOZ_RELEASE_ASSERT(mRecvd->is_sync());
+    if (transact.IsError()) {
+        IPC_LOG("Error: seqno=%d, xid=%d", seqno, transaction);
+        mLastSendError = SyncSendError::ReplyError;
+        return false;
+    }
+
+    IPC_LOG("Got reply: seqno=%d, xid=%d", seqno, transaction);
 
-    *aReply = Move(*mRecvd);
-    mRecvd = nullptr;
-    mLastSendError = SyncSendError::SendSuccess;
+    nsAutoPtr<Message> reply = transact.GetReply();
+
+    MOZ_RELEASE_ASSERT(reply);
+    MOZ_RELEASE_ASSERT(reply->is_reply(), "expected reply");
+    MOZ_RELEASE_ASSERT(!reply->is_reply_error());
+    MOZ_RELEASE_ASSERT(reply->seqno() == seqno);
+    MOZ_RELEASE_ASSERT(reply->type() == replyType, "wrong reply type");
+    MOZ_RELEASE_ASSERT(reply->is_sync());
+
+    *aReply = Move(*reply);
     return true;
 }
 
 bool
 MessageChannel::Call(Message* aMsg, Message* aReply)
 {
     nsAutoPtr<Message> msg(aMsg);
     AssertWorkerThread();
@@ -1247,41 +1432,24 @@ MessageChannel::InterruptEventOccurred()
 }
 
 bool
 MessageChannel::ProcessPendingRequest(const Message &aUrgent)
 {
     AssertWorkerThread();
     mMonitor->AssertCurrentThreadOwns();
 
-    // Note that it is possible we could have sent a sync message at
-    // the same time the parent process sent an urgent message, and
-    // therefore mPendingUrgentRequest is set *and* mRecvd is set as
-    // well, because the link thread received both before the worker
-    // thread woke up.
-    //
-    // In this case, we process the urgent message first, but we need
-    // to save the reply.
-    nsAutoPtr<Message> savedReply(mRecvd.forget());
-
     IPC_LOG("Process pending: seqno=%d, xid=%d", aUrgent.seqno(), aUrgent.transaction_id());
 
     DispatchMessage(aUrgent);
     if (!Connected()) {
         ReportConnectionError("MessageChannel::ProcessPendingRequest");
         return false;
     }
 
-    // In between having dispatched our reply to the parent process, and
-    // re-acquiring the monitor, the parent process could have already
-    // processed that reply and sent the reply to our sync message. If so,
-    // our saved reply should be empty.
-    IPC_ASSERT(!mRecvd || !savedReply, "unknown reply");
-    if (!mRecvd)
-        mRecvd = savedReply.forget();
     return true;
 }
 
 bool
 MessageChannel::DequeueOne(Message *recvd)
 {
     AssertWorkerThread();
     mMonitor->AssertCurrentThreadOwns();
@@ -1347,18 +1515,16 @@ MessageChannel::OnMaybeDequeueOne()
 
     if (IsOnCxxStack() && recvd.is_interrupt() && recvd.is_reply()) {
         // We probably just received a reply in a nested loop for an
         // Interrupt call sent before entering that loop.
         mOutOfTurnReplies[recvd.seqno()] = Move(recvd);
         return false;
     }
 
-    // We should not be in a transaction yet if we're not blocked.
-    MOZ_RELEASE_ASSERT(mCurrentTransaction == 0);
     DispatchMessage(recvd);
 
     return true;
 }
 
 void
 MessageChannel::DispatchMessage(const Message &aMsg)
 {
@@ -1369,17 +1535,17 @@ MessageChannel::DispatchMessage(const Me
     nsAutoPtr<Message> reply;
 
     IPC_LOG("DispatchMessage: seqno=%d, xid=%d", aMsg.seqno(), aMsg.transaction_id());
 
     {
         AutoEnterTransaction transaction(this, aMsg);
 
         int id = aMsg.transaction_id();
-        MOZ_RELEASE_ASSERT(!aMsg.is_sync() || id == mCurrentTransaction);
+        MOZ_RELEASE_ASSERT(!aMsg.is_sync() || id == transaction.TransactionID());
 
         {
             MonitorAutoUnlock unlock(*mMonitor);
             CxxStackFrame frame(*this, IN_MESSAGE, &aMsg);
 
             mListener->ArtificialSleep();
 
             if (aMsg.is_sync())
@@ -1387,17 +1553,17 @@ MessageChannel::DispatchMessage(const Me
             else if (aMsg.is_interrupt())
                 DispatchInterruptMessage(aMsg, 0);
             else
                 DispatchAsyncMessage(aMsg);
 
             mListener->ArtificialSleep();
         }
 
-        if (mCurrentTransaction != id) {
+        if (reply && transaction.IsCanceled()) {
             // The transaction has been canceled. Don't send a reply.
             IPC_LOG("Nulling out reply due to cancellation, seqno=%d, xid=%d", aMsg.seqno(), id);
             reply = nullptr;
         }
     }
 
     if (reply && ChannelConnected == mChannelState) {
         IPC_LOG("Sending reply seqno=%d, xid=%d", aMsg.seqno(), aMsg.transaction_id());
@@ -1415,18 +1581,16 @@ MessageChannel::DispatchSyncMessage(cons
     MOZ_RELEASE_ASSERT(prio == IPC::Message::PRIORITY_NORMAL || NS_IsMainThread());
 
     MessageChannel* dummy;
     MessageChannel*& blockingVar = mSide == ChildSide && NS_IsMainThread() ? gParentProcessBlocker : dummy;
 
     Result rv;
     {
         AutoSetValue<MessageChannel*> blocked(blockingVar, this);
-        AutoSetValue<bool> sync(mDispatchingSyncMessage, true);
-        AutoSetValue<int> prioSet(mDispatchingSyncMessagePriority, prio);
         rv = mListener->OnMessageReceived(aMsg, aReply);
     }
 
     if (!MaybeHandleError(rv, aMsg, "DispatchSyncMessage")) {
         aReply = new Message();
         aReply->set_sync();
         aReply->set_priority(aMsg.priority());
         aReply->set_reply();
@@ -2118,124 +2282,82 @@ MessageChannel::CancelTransaction(int tr
     // canceled. Consequently, we have to update the state variables below.
     //
     // We also need to ensure that when any IPC functions on the stack return,
     // they don't reset these values using an RAII class like AutoSetValue. To
     // avoid that, these RAII classes check if the variable they set has been
     // tampered with (by us). If so, they don't reset the variable to the old
     // value.
 
-    IPC_LOG("CancelTransaction: xid=%d prios=%d", transaction, mPendingSendPriorities);
-
-    if (mPendingSendPriorities & (1 << IPC::Message::PRIORITY_NORMAL)) {
-        // This isn't an assert so much as an intentional crash because we're in
-        // a situation that we don't know how to recover from: The child is
-        // awaiting a reply to a normal-priority sync message. The transaction
-        // that this message initiated has now been canceled. That could only
-        // happen if a CPOW raced with the sync message and was dispatched by
-        // the child while the child was awaiting the sync reply; at some point
-        // while dispatching the CPOW, the transaction was canceled.
-        //
-        // Notes:
-        //
-        // 1. We don't want to cancel the normal-priority sync message along
-        // with the CPOWs because the browser relies on these messages working
-        // reliably.
-        //
-        // 2. Ideally we would like to avoid dispatching CPOWs while awaiting a
-        // sync response. This isn't possible though. To avoid deadlock, the
-        // parent would have to dispatch the sync message while waiting for the
-        // CPOW response. However, it wouldn't have dispatched async messages at
-        // that time, so we would have a message ordering bug. Dispatching the
-        // async messages first causes other hard-to-handle situations (what if
-        // they send CPOWs?).
-        //
-        // 3. We would like to be able to cancel the CPOWs but not the sync
-        // message. However, that would leave both the parent and the child
-        // running code at the same time, all while the sync message is still
-        // outstanding. That can cause a problem where message replies are
-        // received out of order.
-        mListener->IntentionalCrash();
-    }
+    IPC_LOG("CancelTransaction: xid=%d", transaction);
 
     // An unusual case: We timed out a transaction which the other side then
     // cancelled. In this case we just leave the timedout state and try to
     // forget this ever happened.
     if (transaction == mTimedOutMessageSeqno) {
         IPC_LOG("Cancelled timed out message %d", mTimedOutMessageSeqno);
         EndTimeout();
 
         // Normally mCurrentTransaction == 0 here. But it can be non-zero if:
         // 1. Parent sends hi prio message H.
         // 2. Parent times out H.
         // 3. Child dispatches H and sends nested message H' (same transaction).
         // 4. Parent dispatches H' and cancels.
-        MOZ_RELEASE_ASSERT(!mCurrentTransaction || mCurrentTransaction == transaction);
-        mCurrentTransaction = 0;
-
-        // During a timeout Send should always fail.
-        MOZ_RELEASE_ASSERT(!mAwaitingSyncReply);
+        MOZ_RELEASE_ASSERT(!mTransactionStack || mTransactionStack->TransactionID() == transaction);
+        if (mTransactionStack) {
+            mTransactionStack->Cancel();
+        }
     } else {
-        MOZ_RELEASE_ASSERT(mCurrentTransaction == transaction);
-        mCurrentTransaction = 0;
-
-        mAwaitingSyncReply = false;
-        mAwaitingSyncReplyPriority = 0;
+        MOZ_RELEASE_ASSERT(mTransactionStack->TransactionID() == transaction);
+        mTransactionStack->Cancel();
     }
 
     bool foundSync = false;
     for (MessageQueue::iterator it = mPending.begin(); it != mPending.end(); ) {
         Message &msg = *it;
 
         // If there was a race between the parent and the child, then we may
         // have a queued sync message. We want to drop this message from the
-        // queue since it will get cancelled along with the transaction being
-        // cancelled. We don't bother doing this for normal priority messages
-        // because the child is just going to crash in that case, and we want to
-        // avoid processing messages out of order in the short time before it
-        // crashes.
+        // queue since if will get cancelled along with the transaction being
+        // cancelled. This happens if the message in the queue is high priority.
         if (msg.is_sync() && msg.priority() != IPC::Message::PRIORITY_NORMAL) {
             MOZ_RELEASE_ASSERT(!foundSync);
             MOZ_RELEASE_ASSERT(msg.transaction_id() != transaction);
             IPC_LOG("Removing msg from queue seqno=%d xid=%d", msg.seqno(), msg.transaction_id());
             foundSync = true;
             it = mPending.erase(it);
             continue;
         }
 
-        // There may be messages in the queue that we expected to process from
-        // ProcessPendingRequests. However, Send will no longer call that
-        // function once it's been canceled. So we may need to process these
-        // messages in the normal event loop instead.
-        mWorkerLoop->PostTask(FROM_HERE, new DequeueTask(mDequeueOneTask));
-
         it++;
     }
+}
 
-    // We could also zero out mDispatchingSyncMessage here. However, that would
-    // cause a race because mDispatchingSyncMessage is a worker-thread-only
-    // field and we can be called on the I/O thread. Luckily, we can check to
-    // see if mCurrentTransaction is 0 before examining DispatchSyncMessage.
+bool
+MessageChannel::IsInTransaction() const
+{
+    MonitorAutoLock lock(*mMonitor);
+    return !!mTransactionStack;
 }
 
 void
 MessageChannel::CancelCurrentTransaction()
 {
     MonitorAutoLock lock(*mMonitor);
-    if (mCurrentTransaction) {
+    if (DispatchingSyncMessagePriority() >= IPC::Message::PRIORITY_HIGH) {
         if (DispatchingSyncMessagePriority() == IPC::Message::PRIORITY_URGENT ||
             DispatchingAsyncMessagePriority() == IPC::Message::PRIORITY_URGENT)
         {
             mListener->IntentionalCrash();
         }
 
-        IPC_LOG("Cancel requested: current xid=%d", mCurrentTransaction);
+        IPC_LOG("Cancel requested: current xid=%d", CurrentHighPriorityTransaction());
         MOZ_RELEASE_ASSERT(DispatchingSyncMessage());
-        CancelMessage *cancel = new CancelMessage(mCurrentTransaction);
-        CancelTransaction(mCurrentTransaction);
+        CancelMessage *cancel = new CancelMessage(CurrentHighPriorityTransaction());
+        CancelTransaction(CurrentHighPriorityTransaction());
         mLink->SendMessage(cancel);
     }
 }
 
 void
 CancelCPOWs()
 {
     if (gParentProcessBlocker) {
--- a/ipc/glue/MessageChannel.h
+++ b/ipc/glue/MessageChannel.h
@@ -52,16 +52,18 @@ enum class SyncSendError {
     NotConnectedBeforeSend,
     DisconnectedDuringSend,
     CancelledBeforeSend,
     CancelledAfterSend,
     TimedOut,
     ReplyError,
 };
 
+class AutoEnterTransaction;
+
 class MessageChannel : HasResultCodes
 {
     friend class ProcessLink;
     friend class ThreadLink;
 
     class CxxStackFrame;
     class InterruptFrame;
 
@@ -151,17 +153,17 @@ class MessageChannel : HasResultCodes
     }
 
     void SetReplyTimeoutMs(int32_t aTimeoutMs);
 
     bool IsOnCxxStack() const {
         return !mCxxStackFrames.empty();
     }
 
-    bool IsInTransaction() const { return mCurrentTransaction != 0; }
+    bool IsInTransaction() const;
     void CancelCurrentTransaction();
 
     /**
      * This function is used by hang annotation code to determine which IPDL
      * actor is highest in the call stack at the time of the hang. It should
      * be called from the main thread when a sync or intr message is about to
      * be sent.
      */
@@ -258,17 +260,17 @@ class MessageChannel : HasResultCodes
     void Clear();
 
     // Send OnChannelConnected notification to listeners.
     void DispatchOnChannelConnected();
 
     bool InterruptEventOccurred();
     bool HasPendingEvents();
 
-    void ProcessPendingRequests(int seqno, int transaction);
+    void ProcessPendingRequests(AutoEnterTransaction& aTransaction);
     bool ProcessPendingRequest(const Message &aUrgent);
 
     void MaybeUndeferIncall();
     void EnqueuePendingMessages();
 
     // Executed on the worker thread. Dequeues one pending message.
     bool OnMaybeDequeueOne();
     bool DequeueOne(Message *recvd);
@@ -361,25 +363,16 @@ class MessageChannel : HasResultCodes
 
   private:
     // Called from both threads
     size_t InterruptStackDepth() const {
         mMonitor->AssertCurrentThreadOwns();
         return mInterruptStack.size();
     }
 
-    // Returns true if we're blocking waiting for a reply.
-    bool AwaitingSyncReply() const {
-        mMonitor->AssertCurrentThreadOwns();
-        return mAwaitingSyncReply;
-    }
-    int AwaitingSyncReplyPriority() const {
-        mMonitor->AssertCurrentThreadOwns();
-        return mAwaitingSyncReplyPriority;
-    }
     bool AwaitingInterruptReply() const {
         mMonitor->AssertCurrentThreadOwns();
         return !mInterruptStack.empty();
     }
     bool AwaitingIncomingMessage() const {
         mMonitor->AssertCurrentThreadOwns();
         return mIsWaitingForIncoming;
     }
@@ -399,27 +392,17 @@ class MessageChannel : HasResultCodes
             mChannel.mIsWaitingForIncoming = false;
         }
 
     private:
         MessageChannel& mChannel;
     };
     friend class AutoEnterWaitForIncoming;
 
-    // Returns true if we're dispatching a sync message's callback.
-    bool DispatchingSyncMessage() const {
-        AssertWorkerThread();
-        return mDispatchingSyncMessage;
-    }
-
-    int DispatchingSyncMessagePriority() const {
-        AssertWorkerThread();
-        return mDispatchingSyncMessagePriority;
-    }
-
+    // Returns true if we're dispatching an async message's callback.
     bool DispatchingAsyncMessage() const {
         AssertWorkerThread();
         return mDispatchingAsyncMessage;
     }
 
     int DispatchingAsyncMessagePriority() const {
         AssertWorkerThread();
         return mDispatchingAsyncMessagePriority;
@@ -555,25 +538,16 @@ class MessageChannel : HasResultCodes
             }
         }
       private:
         T& mVar;
         T mPrev;
         T mNew;
     };
 
-    // Worker thread only.
-    bool mAwaitingSyncReply;
-    int mAwaitingSyncReplyPriority;
-
-    // Set while we are dispatching a synchronous message. Only for use on the
-    // worker thread.
-    bool mDispatchingSyncMessage;
-    int mDispatchingSyncMessagePriority;
-
     bool mDispatchingAsyncMessage;
     int mDispatchingAsyncMessagePriority;
 
     // When we send an urgent request from the parent process, we could race
     // with an RPC message that was issued by the child beforehand. In this
     // case, if the parent were to wake up while waiting for the urgent reply,
     // and process the RPC, it could send an additional urgent message. The
     // child would wake up to process the urgent message (as it always will),
@@ -585,66 +559,26 @@ class MessageChannel : HasResultCodes
     // chain of RPC/urgent messages, it allocates a new transaction ID. Any
     // messages the parent receives, not apart of this transaction, are
     // deferred. When issuing RPC/urgent requests on top of a started
     // transaction, the initiating transaction ID is used.
     //
     // To ensure IDs are unique, we use sequence numbers for transaction IDs,
     // which grow in opposite directions from child to parent.
 
-    // The current transaction ID.
-    int32_t mCurrentTransaction;
+    friend class AutoEnterTransaction;
+    AutoEnterTransaction *mTransactionStack;
 
-    // This field describes the priorities of the sync Send calls that are
-    // currently on stack. If a Send call for a message with priority P is on
-    // the C stack, then mPendingSendPriorities & (1 << P) will be
-    // non-zero. Note that cancelled Send calls are not removed from this
-    // bitfield (until they return).
-    int mPendingSendPriorities;
+    int32_t CurrentHighPriorityTransaction() const;
 
-    class AutoEnterTransaction
-    {
-     public:
-       explicit AutoEnterTransaction(MessageChannel *aChan, int32_t aMsgSeqno)
-        : mChan(aChan),
-          mNewTransaction(INT32_MAX),
-          mOldTransaction(mChan->mCurrentTransaction)
-       {
-           mChan->mMonitor->AssertCurrentThreadOwns();
-           if (mChan->mCurrentTransaction == 0) {
-               mNewTransaction = aMsgSeqno;
-               mChan->mCurrentTransaction = aMsgSeqno;
-           }
-       }
-       explicit AutoEnterTransaction(MessageChannel *aChan, const Message &aMessage)
-        : mChan(aChan),
-          mNewTransaction(aMessage.transaction_id()),
-          mOldTransaction(mChan->mCurrentTransaction)
-       {
-           mChan->mMonitor->AssertCurrentThreadOwns();
+    bool AwaitingSyncReply() const;
+    int AwaitingSyncReplyPriority() const;
 
-           if (!aMessage.is_sync())
-               return;
-
-           MOZ_DIAGNOSTIC_ASSERT(
-               !(mChan->mSide == ParentSide && mOldTransaction != aMessage.transaction_id()) ||
-               !mOldTransaction || aMessage.priority() > mChan->AwaitingSyncReplyPriority());
-           mChan->mCurrentTransaction = aMessage.transaction_id();
-       }
-       ~AutoEnterTransaction() {
-           mChan->mMonitor->AssertCurrentThreadOwns();
-           if (mChan->mCurrentTransaction == mNewTransaction) {
-               mChan->mCurrentTransaction = mOldTransaction;
-           }
-       }
-
-      private:
-       MessageChannel *mChan;
-       int32_t mNewTransaction, mOldTransaction;
-    };
+    bool DispatchingSyncMessage() const;
+    int DispatchingSyncMessagePriority() const;
 
     // If a sync message times out, we store its sequence number here. Any
     // future sync messages will fail immediately. Once the reply for original
     // sync message is received, we allow sync messages again.
     //
     // When a message times out, nothing is done to inform the other side. The
     // other side will eventually dispatch the message and send a reply. Our
     // side is responsible for replying to all sync messages sent by the other
@@ -652,24 +586,16 @@ class MessageChannel : HasResultCodes
     // error.
     //
     // A message is only timed out if it initiated a transaction. This avoids
     // hitting a lot of corner cases with message nesting that we don't really
     // care about.
     int32_t mTimedOutMessageSeqno;
     int mTimedOutMessagePriority;
 
-    // If waiting for the reply to a sync out-message, it will be saved here
-    // on the I/O thread and then read and cleared by the worker thread.
-    nsAutoPtr<Message> mRecvd;
-
-    // If a sync message reply that is an error arrives, we increment this
-    // counter rather than storing it in mRecvd.
-    size_t mRecvdErrors;
-
     // Queue of all incoming messages, except for replies to sync and urgent
     // messages, which are delivered directly to mRecvd, and any pending urgent
     // incall, which is stored in mPendingUrgentRequest.
     //
     // If both this side and the other side are functioning correctly, the queue
     // can only be in certain configurations.  Let
     //
     //   |A<| be an async in-message,
--- a/ipc/ipdl/test/cxx/TestDemon.cpp
+++ b/ipc/ipdl/test/cxx/TestDemon.cpp
@@ -202,27 +202,41 @@ TestDemonParent::RunLimitedSequence(int 
       gStackHeight--;
       return;
     }
   }
 
   gStackHeight--;
 }
 
+static bool
+AllowAsync(int outgoing, int incoming)
+{
+  return incoming >= outgoing - 5;
+}
+
 bool
 TestDemonParent::DoAction(int flags)
 {
   if (flags & ASYNC_ONLY) {
-    DEMON_LOG("SendAsyncMessage [%d]", mOutgoing[0]);
-    return SendAsyncMessage(mOutgoing[0]++);
+    if (AllowAsync(mOutgoing[0], mIncoming[0])) {
+      DEMON_LOG("SendAsyncMessage [%d]", mOutgoing[0]);
+      return SendAsyncMessage(mOutgoing[0]++);
+    } else {
+      return true;
+    }
   } else {
 	switch (Choose(3)) {
      case 0:
-      DEMON_LOG("SendAsyncMessage [%d]", mOutgoing[0]);
-      return SendAsyncMessage(mOutgoing[0]++);
+      if (AllowAsync(mOutgoing[0], mIncoming[0])) {
+        DEMON_LOG("SendAsyncMessage [%d]", mOutgoing[0]);
+        return SendAsyncMessage(mOutgoing[0]++);
+      } else {
+        return true;
+      }
 
      case 1: {
        DEMON_LOG("Start SendHiPrioSyncMessage");
        bool r = SendHiPrioSyncMessage();
        DEMON_LOG("End SendHiPrioSyncMessage result=%d", r);
        return r;
      }
 
@@ -334,18 +348,22 @@ TestDemonChild::RunLimitedSequence()
   gStackHeight--;
 }
 
 bool
 TestDemonChild::DoAction()
 {
   switch (Choose(6)) {
    case 0:
-	DEMON_LOG("SendAsyncMessage [%d]", mOutgoing[0]);
-	return SendAsyncMessage(mOutgoing[0]++);
+    if (AllowAsync(mOutgoing[0], mIncoming[0])) {
+      DEMON_LOG("SendAsyncMessage [%d]", mOutgoing[0]);
+      return SendAsyncMessage(mOutgoing[0]++);
+    } else {
+      return true;
+    }
 
    case 1: {
      DEMON_LOG("Start SendHiPrioSyncMessage");
      bool r = SendHiPrioSyncMessage();
      DEMON_LOG("End SendHiPrioSyncMessage result=%d", r);
      return r;
    }
 
--- a/js/src/asmjs/AsmJS.cpp
+++ b/js/src/asmjs/AsmJS.cpp
@@ -2645,17 +2645,17 @@ class MOZ_STACK_CLASS FunctionValidator
 
     bool init(PropertyName* name, unsigned line) {
         if (!locals_.init() || !breakLabels_.init() || !continueLabels_.init())
             return false;
 
         if (!m_.mg().startFuncDef(line, &fg_))
             return false;
 
-        encoder_.emplace(fg_.bytecode());
+        encoder_.emplace(fg_.bytes());
         return true;
     }
 
     bool finish(uint32_t funcIndex, unsigned generateTime) {
         return m_.mg().finishFuncDef(funcIndex, generateTime, &fg_);
     }
 
     bool fail(ParseNode* pn, const char* str) {
@@ -3527,23 +3527,18 @@ CheckVariables(FunctionValidator& f, Par
         for (ParseNode* var = VarListHead(stmt); var; var = NextNode(var)) {
             if (!CheckVariable(f, var, &types, &inits))
                 return false;
         }
     }
 
     MOZ_ASSERT(f.encoder().empty());
 
-    if (!f.encoder().writeVarU32(types.length()))
-        return false;
-
-    for (ValType v : types) {
-        if (!f.encoder().writeValType(v))
-            return false;
-    }
+    if (!EncodeLocalEntries(f.encoder(), types))
+        return false;
 
     for (uint32_t i = 0; i < inits.length(); i++) {
         NumLit lit = inits[i];
         if (lit.isZeroBits())
             continue;
         if (!f.encoder().writeExpr(Expr::SetLocal))
             return false;
         if (!f.encoder().writeVarU32(firstVar + i))
@@ -6418,17 +6413,17 @@ CheckSwitchRange(FunctionValidator& f, P
         if (!CheckCaseExpr(f, CaseExpr(stmt), &i))
             return false;
 
         *low = Min(*low, i);
         *high = Max(*high, i);
     }
 
     int64_t i64 = (int64_t(*high) - int64_t(*low)) + 1;
-    if (i64 > 4 * 1024 * 1024)
+    if (i64 > MaxBrTableElems)
         return f.fail(initialStmt, "all switch statements generate tables; this table would be too big");
 
     *tableLength = uint32_t(i64);
     return true;
 }
 
 static bool
 CheckSwitchExpr(FunctionValidator& f, ParseNode* switchExpr)
--- a/js/src/asmjs/Wasm.cpp
+++ b/js/src/asmjs/Wasm.cpp
@@ -139,68 +139,46 @@ static bool
 CheckType(FunctionDecoder& f, ExprType actual, ExprType expected)
 {
     MOZ_ASSERT(expected != AnyType);
     return expected == ExprType::Void ||
            CheckType(f, actual, NonVoidToValType(expected));
 }
 
 static bool
-DecodeExpr(FunctionDecoder& f, ExprType* type);
-
-static bool
-DecodeValType(JSContext* cx, Decoder& d, ValType *type)
+CheckValType(JSContext* cx, Decoder& d, ValType type)
 {
-    if (!d.readValType(type))
-        return Fail(cx, d, "bad value type");
-
-    switch (*type) {
+    switch (type) {
       case ValType::I32:
       case ValType::F32:
       case ValType::F64:
         return true;
       case ValType::I64:
 #ifndef JS_CPU_X64
         return Fail(cx, d, "i64 NYI on this platform");
 #endif
         return true;
       default:
         // Note: it's important not to remove this default since readValType()
         // can return ValType values for which there is no enumerator.
         break;
     }
 
-    return Fail(cx, "bad value type");
+    return Fail(cx, d, "bad value type");
 }
 
 static bool
-DecodeExprType(JSContext* cx, Decoder& d, ExprType *type)
+CheckExprType(JSContext* cx, Decoder& d, ExprType type)
 {
-    if (!d.readExprType(type))
-        return Fail(cx, d, "bad expression type");
+    return type == ExprType::Void ||
+           CheckValType(cx, d, NonVoidToValType(type));
+}
 
-    switch (*type) {
-      case ExprType::I32:
-      case ExprType::F32:
-      case ExprType::F64:
-      case ExprType::Void:
-        return true;
-      case ExprType::I64:
-#ifndef JS_CPU_X64
-        return Fail(cx, d, "i64 NYI on this platform");
-#endif
-        return true;
-      default:
-        // Note: it's important not to remove this default since readExprType()
-        // can return ExprType values for which there is no enumerator.
-        break;
-    }
-
-    return Fail(cx, "bad expression type");
-}
+static bool
+DecodeExpr(FunctionDecoder& f, ExprType* type);
 
 static bool
 DecodeNop(FunctionDecoder& f, ExprType* type)
 {
     *type = ExprType::Void;
     return true;
 }
 
@@ -264,27 +242,29 @@ DecodeCallIndirect(FunctionDecoder& f, E
         return false;
 
     return DecodeCallWithSig(f, f.mg().sig(sigIndex), type);
 }
 
 static bool
 DecodeConstI32(FunctionDecoder& f, ExprType* type)
 {
-    if (!f.d().readVarU32())
+    uint32_t _;
+    if (!f.d().readVarU32(&_))
         return f.fail("unable to read i32.const immediate");
 
     *type = ExprType::I32;
     return true;
 }
 
 static bool
 DecodeConstI64(FunctionDecoder& f, ExprType* type)
 {
-    if (!f.d().readVarU64())
+    uint64_t _;
+    if (!f.d().readVarU64(&_))
         return f.fail("unable to read i64.const immediate");
 
     *type = ExprType::I64;
     return true;
 }
 
 static bool
 DecodeConstF32(FunctionDecoder& f, ExprType* type)
@@ -851,34 +831,16 @@ DecodeExpr(FunctionDecoder& f, ExprType*
         // can return Expr values for which there is no enumerator.
         break;
     }
 
     return f.fail("bad expression code");
 }
 
 /*****************************************************************************/
-// dynamic link data
-
-struct ImportName
-{
-    UniqueChars module;
-    UniqueChars func;
-
-    ImportName(UniqueChars module, UniqueChars func)
-      : module(Move(module)), func(Move(func))
-    {}
-    ImportName(ImportName&& rhs)
-      : module(Move(rhs.module)), func(Move(rhs.func))
-    {}
-};
-
-typedef Vector<ImportName, 0, SystemAllocPolicy> ImportNameVector;
-
-/*****************************************************************************/
 // wasm decoding and generation
 
 typedef HashSet<const DeclaredSig*, SigHashPolicy> SigSet;
 
 static bool
 DecodeSignatures(JSContext* cx, Decoder& d, ModuleGeneratorData* init)
 {
     uint32_t sectionStart;
@@ -907,25 +869,31 @@ DecodeSignatures(JSContext* cx, Decoder&
         uint32_t numArgs;
         if (!d.readVarU32(&numArgs))
             return Fail(cx, d, "bad number of signature args");
 
         if (numArgs > MaxArgsPerFunc)
             return Fail(cx, d, "too many arguments in signature");
 
         ExprType result;
-        if (!DecodeExprType(cx, d, &result))
+        if (!d.readExprType(&result))
+            return Fail(cx, d, "bad expression type");
+
+        if (!CheckExprType(cx, d, result))
             return false;
 
         ValTypeVector args;
         if (!args.resize(numArgs))
             return false;
 
         for (uint32_t i = 0; i < numArgs; i++) {
-            if (!DecodeValType(cx, d, &args[i]))
+            if (!d.readValType(&args[i]))
+                return Fail(cx, d, "bad value type");
+
+            if (!CheckValType(cx, d, args[i]))
                 return false;
         }
 
         init->sigs[sigIndex] = Sig(Move(args), result);
 
         SigSet::AddPtr p = dupSet.lookupForAdd(init->sigs[sigIndex]);
         if (p)
             return Fail(cx, d, "duplicate signature");
@@ -1053,38 +1021,53 @@ CheckTypeForJS(JSContext* cx, Decoder& d
     }
 
     if (sig.ret() == ExprType::I64)
         return Fail(cx, d, "cannot import/export i64 return type");
 
     return true;
 }
 
+struct ImportName
+{
+    Bytes module;
+    Bytes func;
+
+    ImportName(Bytes&& module, Bytes&& func)
+      : module(Move(module)), func(Move(func))
+    {}
+    ImportName(ImportName&& rhs)
+      : module(Move(rhs.module)), func(Move(rhs.func))
+    {}
+};
+
+typedef Vector<ImportName, 0, SystemAllocPolicy> ImportNameVector;
+
 static bool
 DecodeImport(JSContext* cx, Decoder& d, ModuleGeneratorData* init, ImportNameVector* importNames)
 {
     const DeclaredSig* sig;
     if (!DecodeSignatureIndex(cx, d, *init, &sig))
         return false;
 
     if (!init->imports.emplaceBack(sig))
         return false;
 
     if (!CheckTypeForJS(cx, d, *sig))
         return false;
 
-    UniqueChars moduleName = d.readCString();
-    if (!moduleName)
+    Bytes moduleName;
+    if (!d.readBytes(&moduleName))
         return Fail(cx, d, "expected import module name");
 
-    if (!*moduleName.get())
+    if (moduleName.empty())
         return Fail(cx, d, "module name cannot be empty");
 
-    UniqueChars funcName = d.readCString();
-    if (!funcName)
+    Bytes funcName;
+    if (!d.readBytes(&funcName))
         return Fail(cx, d, "expected import func name");
 
     return importNames->emplaceBack(Move(moduleName), Move(funcName));
 }
 
 static bool
 DecodeImportTable(JSContext* cx, Decoder& d, ModuleGeneratorData* init, ImportNameVector* importNames)
 {
@@ -1159,24 +1142,36 @@ DecodeMemory(JSContext* cx, Decoder& d, 
 
     mg.initHeapUsage(HeapUsage::Unshared);
     return true;
 }
 
 typedef HashSet<const char*, CStringHasher> CStringSet;
 
 static UniqueChars
-DecodeFieldName(JSContext* cx, Decoder& d, CStringSet* dupSet)
+DecodeExportName(JSContext* cx, Decoder& d, CStringSet* dupSet)
 {
-    UniqueChars fieldName = d.readCString();
-    if (!fieldName) {
-        Fail(cx, d, "expected export external name string");
+    Bytes fieldBytes;
+    if (!d.readBytes(&fieldBytes)) {
+        Fail(cx, d, "expected export name");
         return nullptr;
     }
 
+    if (memchr(fieldBytes.begin(), 0, fieldBytes.length())) {
+        Fail(cx, d, "null in export names not yet supported");
+        return nullptr;
+    }
+
+    if (!fieldBytes.append(0))
+        return nullptr;
+
+    UniqueChars fieldName((char*)fieldBytes.extractRawBuffer());
+    if (!fieldName)
+        return nullptr;
+
     CStringSet::AddPtr p = dupSet->lookupForAdd(fieldName.get());
     if (p) {
         Fail(cx, d, "duplicate export");
         return nullptr;
     }
 
     if (!dupSet->add(p, fieldName.get()))
         return nullptr;
@@ -1192,17 +1187,17 @@ DecodeFunctionExport(JSContext* cx, Deco
         return Fail(cx, d, "expected export internal index");
 
     if (funcIndex >= mg.numFuncSigs())
         return Fail(cx, d, "export function index out of range");
 
     if (!CheckTypeForJS(cx, d, mg.funcSig(funcIndex)))
         return false;
 
-    UniqueChars fieldName = DecodeFieldName(cx, d, dupSet);
+    UniqueChars fieldName = DecodeExportName(cx, d, dupSet);
     if (!fieldName)
         return false;
 
     return mg.declareExport(Move(fieldName), funcIndex);
 }
 
 static bool
 DecodeExportTable(JSContext* cx, Decoder& d, ModuleGenerator& mg)
@@ -1253,25 +1248,21 @@ DecodeFunctionBody(JSContext* cx, Decode
     FunctionGenerator fg;
     if (!mg.startFuncDef(d.currentOffset(), &fg))
         return false;
 
     ValTypeVector locals;
     if (!locals.appendAll(mg.funcSig(funcIndex).args()))
         return false;
 
-    uint32_t numVars;
-    if (!d.readVarU32(&numVars))
-        return Fail(cx, d, "expected number of local vars");
+    if (!DecodeLocalEntries(d, &locals))
+        return Fail(cx, d, "failed decoding local entries");
 
-    for (uint32_t i = 0; i < numVars; i++) {
-        ValType type;
-        if (!DecodeValType(cx, d, &type))
-            return false;
-        if (!locals.append(type))
+    for (ValType type : locals) {
+        if (!CheckValType(cx, d, type))
             return false;
     }
 
     FunctionDecoder f(cx, d, mg, fg, funcIndex, locals);
 
     ExprType type = ExprType::Void;
 
     while (d.currentPosition() < bodyEnd) {
@@ -1280,20 +1271,20 @@ DecodeFunctionBody(JSContext* cx, Decode
     }
 
     if (!CheckType(f, type, f.sig().ret()))
         return false;
 
     if (d.currentPosition() != bodyEnd)
         return Fail(cx, d, "function body length mismatch");
 
-    if (!fg.bytecode().resize(bodySize))
+    if (!fg.bytes().resize(bodySize))
         return false;
 
-    memcpy(fg.bytecode().begin(), bodyBegin, bodySize);
+    memcpy(fg.bytes().begin(), bodyBegin, bodySize);
 
     int64_t after = PRMJ_Now();
     unsigned generateTime = (after - before) / PRMJ_USEC_PER_MSEC;
 
     return mg.finishFuncDef(funcIndex, generateTime, &fg);
 }
 
 static bool
@@ -1355,17 +1346,17 @@ DecodeDataSegments(JSContext* cx, Decode
         uint32_t numBytes;
         if (!d.readVarU32(&numBytes))
             return Fail(cx, d, "expected segment size");
 
         if (dstOffset > heapLength || heapLength - dstOffset < numBytes)
             return Fail(cx, d, "data segment does not fit in memory");
 
         const uint8_t* src;
-        if (!d.readRawData(numBytes, &src))
+        if (!d.readBytesRaw(numBytes, &src))
             return Fail(cx, d, "data segment shorter than declared");
 
         memcpy(heapBase + dstOffset, src, numBytes);
         prevEnd = dstOffset + numBytes;
     }
 
     if (!d.finishSection(sectionStart))
         return Fail(cx, d, "data section byte size mismatch");
@@ -1467,44 +1458,44 @@ CheckCompilerSupport(JSContext* cx)
 #endif
         JS_ReportError(cx, "WebAssembly is not supported on the current device.");
         return false;
     }
     return true;
 }
 
 static bool
-GetProperty(JSContext* cx, HandleObject obj, const char* utf8Chars, MutableHandleValue v)
+GetProperty(JSContext* cx, HandleObject obj, const Bytes& bytes, MutableHandleValue v)
 {
-    JSAtom* atom = AtomizeUTF8Chars(cx, utf8Chars, strlen(utf8Chars));
+    JSAtom* atom = AtomizeUTF8Chars(cx, (char*)bytes.begin(), bytes.length());
     if (!atom)
         return false;
 
     RootedId id(cx, AtomToId(atom));
     return GetProperty(cx, obj, obj, id, v);
 }
 
 static bool
 ImportFunctions(JSContext* cx, HandleObject importObj, const ImportNameVector& importNames,
                 MutableHandle<FunctionVector> imports)
 {
     if (!importNames.empty() && !importObj)
         return Fail(cx, "no import object given");
 
     for (const ImportName& name : importNames) {
         RootedValue v(cx);
-        if (!GetProperty(cx, importObj, name.module.get(), &v))
+        if (!GetProperty(cx, importObj, name.module, &v))
             return false;
 
-        if (*name.func.get()) {
+        if (!name.func.empty()) {
             if (!v.isObject())
                 return Fail(cx, "import object field is not an Object");
 
             RootedObject obj(cx, &v.toObject());
-            if (!GetProperty(cx, obj, name.func.get(), &v))
+            if (!GetProperty(cx, obj, name.func, &v))
                 return false;
         }
 
         if (!IsFunctionObject(v))
             return Fail(cx, "import object field is not a Function");
 
         if (!imports.append(&v.toObject().as<JSFunction>()))
             return false;
@@ -1539,16 +1530,17 @@ wasm::Eval(JSContext* cx, Handle<TypedAr
     UniqueChars file;
     if (!DescribeScriptedCaller(cx, &file))
         return false;
 
     ImportNameVector importNames;
     UniqueExportMap exportMap;
     Rooted<ArrayBufferObject*> heap(cx);
     Rooted<WasmModuleObject*> moduleObj(cx);
+
     if (!DecodeModule(cx, Move(file), bytes, length, &importNames, &exportMap, &heap, &moduleObj)) {
         if (!cx->isExceptionPending())
             ReportOutOfMemory(cx);
         return false;
     }
 
     Rooted<FunctionVector> imports(cx, FunctionVector(cx));
     if (!ImportFunctions(cx, importObj, importNames, &imports))
new file mode 100644
--- /dev/null
+++ b/js/src/asmjs/WasmBinary.cpp
@@ -0,0 +1,87 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 4 -*-
+ * vim: set ts=8 sts=4 et sw=4 tw=99:
+ *
+ * Copyright 2016 Mozilla Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "asmjs/WasmBinary.h"
+
+#include "asmjs/WasmTypes.h"
+
+using namespace js;
+using namespace js::wasm;
+
+bool
+wasm::EncodeLocalEntries(Encoder& e, const ValTypeVector& locals)
+{
+    uint32_t numLocalEntries = 0;
+    ValType prev = ValType::Limit;
+    for (ValType t : locals) {
+        if (t != prev) {
+            numLocalEntries++;
+            prev = t;
+        }
+    }
+
+    if (!e.writeVarU32(numLocalEntries))
+        return false;
+
+    if (numLocalEntries) {
+        prev = locals[0];
+        uint32_t count = 1;
+        for (uint32_t i = 1; i < locals.length(); i++, count++) {
+            if (prev != locals[i]) {
+                if (!e.writeVarU32(count))
+                    return false;
+                if (!e.writeValType(prev))
+                    return false;
+                prev = locals[i];
+                count = 0;
+            }
+        }
+        if (!e.writeVarU32(count))
+            return false;
+        if (!e.writeValType(prev))
+            return false;
+    }
+
+    return true;
+}
+
+bool
+wasm::DecodeLocalEntries(Decoder& d, ValTypeVector* locals)
+{
+    uint32_t numLocalEntries;
+    if (!d.readVarU32(&numLocalEntries))
+        return false;
+
+    for (uint32_t i = 0; i < numLocalEntries; i++) {
+        uint32_t count;
+        if (!d.readVarU32(&count))
+            return false;
+
+        if (MaxLocals - locals->length() < count)
+            return false;
+
+        ValType type;
+        if (!d.readValType(&type))
+            return false;
+
+        if (!locals->appendN(type, count))
+            return false;
+    }
+
+    return true;
+}
--- a/js/src/asmjs/WasmBinary.h
+++ b/js/src/asmjs/WasmBinary.h
@@ -14,25 +14,21 @@
  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
 
 #ifndef wasm_binary_h
 #define wasm_binary_h
 
-#include "mozilla/DebugOnly.h"
-
 #include "builtin/SIMD.h"
 
 namespace js {
 namespace wasm {
 
-using mozilla::DebugOnly;
-
 static const uint32_t MagicNumber        = 0x6d736100; // "\0asm"
 static const uint32_t EncodingVersion    = 0xa;
 
 static const char SignaturesId[]         = "signatures";
 static const char ImportTableId[]        = "import_table";
 static const char FunctionSignaturesId[] = "function_signatures";
 static const char FunctionTableId[]      = "function_table";
 static const char MemoryId[]             = "memory";
@@ -323,40 +319,39 @@ enum class ExprType
     F32x4 = uint8_t(ValType::F32x4),
     B32x4 = uint8_t(ValType::B32x4),
 
     Limit
 };
 
 typedef int32_t I32x4[4];
 typedef float F32x4[4];
-typedef Vector<uint8_t, 0, SystemAllocPolicy> Bytecode;
-typedef UniquePtr<Bytecode> UniqueBytecode;
+typedef Vector<uint8_t, 0, SystemAllocPolicy> Bytes;
 
-// The Encoder class appends bytes to the Bytecode object it is given during
-// construction. The client is responsible for the Bytecode's lifetime and must
-// keep the Bytecode alive as long as the Encoder is used.
+// The Encoder class appends bytes to the Bytes object it is given during
+// construction. The client is responsible for the Bytes's lifetime and must
+// keep the Bytes alive as long as the Encoder is used.
 
 class Encoder
 {
-    Bytecode& bytecode_;
+    Bytes& bytes_;
 
     template <class T>
     MOZ_WARN_UNUSED_RESULT bool write(const T& v) {
-        return bytecode_.append(reinterpret_cast<const uint8_t*>(&v), sizeof(T));
+        return bytes_.append(reinterpret_cast<const uint8_t*>(&v), sizeof(T));
     }
 
     template <typename UInt>
     MOZ_WARN_UNUSED_RESULT bool writeVarU(UInt i) {
         do {
             uint8_t byte = i & 0x7F;
             i >>= 7;
             if (i != 0)
                 byte |= 0x80;
-            if (!bytecode_.append(byte))
+            if (!bytes_.append(byte))
                 return false;
         } while(i != 0);
         return true;
     }
 
     template <class T>
     MOZ_WARN_UNUSED_RESULT bool writeEnum(T v) {
         static_assert(uint32_t(T::Limit) <= UINT32_MAX, "fits");
@@ -369,52 +364,49 @@ class Encoder
             uint8_t assertByte = assertBits & 0x7f;
             uint8_t patchByte = patchBits & 0x7f;
             assertBits >>= 7;
             patchBits >>= 7;
             if (assertBits != 0) {
                 assertByte |= 0x80;
                 patchByte |= 0x80;
             }
-            MOZ_ASSERT(assertByte == bytecode_[offset]);
-            bytecode_[offset] = patchByte;
+            MOZ_ASSERT(assertByte == bytes_[offset]);
+            bytes_[offset] = patchByte;
             offset++;
         } while(assertBits != 0);
     }
 
     uint32_t varU32ByteLength(size_t offset) const {
         size_t start = offset;
-        while (bytecode_[offset] & 0x80)
+        while (bytes_[offset] & 0x80)
             offset++;
         return offset - start + 1;
     }
-    static const uint32_t EnumSentinel = 0x3fff;
 
     template <class T>
     MOZ_WARN_UNUSED_RESULT bool writePatchableEnum(size_t* offset) {
-        static_assert(uint32_t(T::Limit) <= EnumSentinel, "reserve enough bits");
-        *offset = bytecode_.length();
-        return writeVarU32(EnumSentinel);
+        *offset = bytes_.length();
+        return writeVarU32(uint32_t(T::Limit));
     }
 
     template <class T>
     void patchEnum(size_t offset, T v) {
-        static_assert(uint32_t(T::Limit) <= UINT32_MAX, "fits");
         MOZ_ASSERT(uint32_t(v) < uint32_t(T::Limit));
-        return patchVarU32(offset, uint32_t(v), EnumSentinel);
+        return patchVarU32(offset, uint32_t(v), uint32_t(T::Limit));
     }
 
   public:
-    explicit Encoder(Bytecode& bytecode)
-      : bytecode_(bytecode)
+    explicit Encoder(Bytes& bytes)
+      : bytes_(bytes)
     {
         MOZ_ASSERT(empty());
     }
 
-    size_t currentOffset() const { return bytecode_.length(); }
+    size_t currentOffset() const { return bytes_.length(); }
     bool empty() const { return currentOffset() == 0; }
 
     // Fixed-size encoding operations simply copy the literal bytes (without
     // attempting to align).
 
     MOZ_WARN_UNUSED_RESULT bool writeFixedU32(uint32_t i) {
         return write<uint32_t>(i);
     }
@@ -447,80 +439,78 @@ class Encoder
     }
     MOZ_WARN_UNUSED_RESULT bool writeExprType(ExprType type) {
         return writeEnum(type);
     }
 
     // Variable-length encodings that allow back-patching.
 
     MOZ_WARN_UNUSED_RESULT bool writePatchableVarU32(size_t* offset) {
-        *offset = bytecode_.length();
+        *offset = bytes_.length();
         return writeVarU32(UINT32_MAX);
     }
     void patchVarU32(size_t offset, uint32_t patchBits) {
         return patchVarU32(offset, patchBits, UINT32_MAX);
     }
 
     MOZ_WARN_UNUSED_RESULT bool writePatchableVarU8(size_t* offset) {
-        *offset = bytecode_.length();
+        *offset = bytes_.length();
         return writeU8(UINT8_MAX);
     }
     void patchVarU8(size_t offset, uint8_t patchBits) {
         MOZ_ASSERT(patchBits < 0x80);
         return patchU8(offset, patchBits);
     }
 
     MOZ_WARN_UNUSED_RESULT bool writePatchableExpr(size_t* offset) {
         return writePatchableEnum<Expr>(offset);
     }
     void patchExpr(size_t offset, Expr expr) {
         patchEnum(offset, expr);
     }
 
-    // C-strings are written in UTF8 and null-terminated while raw data can
-    // contain nulls and instead has an explicit byte length.
+    // Byte ranges start with an LEB128 length followed by an arbitrary sequence
+    // of bytes. When used for strings, bytes are to be interpreted as utf8.
 
-    MOZ_WARN_UNUSED_RESULT bool writeCString(const char* cstr) {
-        return bytecode_.append(reinterpret_cast<const uint8_t*>(cstr), strlen(cstr) + 1);
-    }
-    MOZ_WARN_UNUSED_RESULT bool writeRawData(const uint8_t* bytes, uint32_t numBytes) {
-        return bytecode_.append(bytes, numBytes);
+    MOZ_WARN_UNUSED_RESULT bool writeBytes(const void* bytes, uint32_t numBytes) {
+        return writeVarU32(numBytes) &&
+               bytes_.append(reinterpret_cast<const uint8_t*>(bytes), numBytes);
     }
 
     // A "section" is a contiguous range of bytes that stores its own size so
     // that it may be trivially skipped without examining the contents. Sections
     // require backpatching since the size of the section is only known at the
-    // end while the size's uint32 must be stored at the beginning. Immediately
+    // end while the size's varU32 must be stored at the beginning. Immediately
     // after the section length is the string id of the section.
 
     template <size_t IdSizeWith0>
     MOZ_WARN_UNUSED_RESULT bool startSection(const char (&id)[IdSizeWith0], size_t* offset) {
         static const size_t IdSize = IdSizeWith0 - 1;
         MOZ_ASSERT(id[IdSize] == '\0');
         return writePatchableVarU32(offset) &&
                writeVarU32(IdSize) &&
-               writeRawData(reinterpret_cast<const uint8_t*>(id), IdSize);
+               bytes_.append(reinterpret_cast<const uint8_t*>(id), IdSize);
     }
     void finishSection(size_t offset) {
-        return patchVarU32(offset, bytecode_.length() - offset - varU32ByteLength(offset));
+        return patchVarU32(offset, bytes_.length() - offset - varU32ByteLength(offset));
     }
 
     // Temporary encoding forms which should be removed as part of the
     // conversion to wasm:
 
     MOZ_WARN_UNUSED_RESULT bool writeU8(uint8_t i) {
         return write<uint8_t>(i);
     }
     MOZ_WARN_UNUSED_RESULT bool writePatchableU8(size_t* offset) {
-        *offset = bytecode_.length();
-        return bytecode_.append(0xff);
+        *offset = bytes_.length();
+        return bytes_.append(0xff);
     }
     void patchU8(size_t offset, uint8_t i) {
-        MOZ_ASSERT(bytecode_[offset] == 0xff);
-        bytecode_[offset] = i;
+        MOZ_ASSERT(bytes_[offset] == 0xff);
+        bytes_[offset] = i;
     }
 };
 
 // The Decoder class decodes the bytes in the range it is given during
 // construction. The client is responsible for keeping the byte range alive as
 // long as the Decoder is used.
 
 class Decoder
@@ -528,30 +518,28 @@ class Decoder
     const uint8_t* const beg_;
     const uint8_t* const end_;
     const uint8_t* cur_;
 
     template <class T>
     MOZ_WARN_UNUSED_RESULT bool read(T* out) {
         if (bytesRemain() < sizeof(T))
             return false;
-        if (out)
-            memcpy((void*)out, cur_, sizeof(T));
+        memcpy((void*)out, cur_, sizeof(T));
         cur_ += sizeof(T);
         return true;
     }
 
     template <class T>
     MOZ_WARN_UNUSED_RESULT bool readEnum(T* out) {
         static_assert(uint32_t(T::Limit) <= UINT32_MAX, "fits");
         uint32_t u32;
         if (!readVarU32(&u32) || u32 >= uint32_t(T::Limit))
             return false;
-        if (out)
-            *out = T(u32);
+        *out = T(u32);
         return true;
     }
 
     template <class T>
     T uncheckedRead() {
         MOZ_ASSERT(bytesRemain() >= sizeof(T));
         T ret;
         memcpy(&ret, cur_, sizeof(T));
@@ -561,53 +549,51 @@ class Decoder
 
     template <class T>
     T uncheckedReadEnum() {
         static_assert(uint32_t(T::Limit) <= UINT32_MAX, "fits");
         return (T)uncheckedReadVarU32();
     }
 
     template <typename UInt>
-    MOZ_WARN_UNUSED_RESULT bool readVarU(UInt* out = nullptr) {
+    MOZ_WARN_UNUSED_RESULT bool readVarU(UInt* out) {
         const unsigned numBits = sizeof(UInt) * CHAR_BIT;
         const unsigned remainderBits = numBits % 7;
         const unsigned numBitsInSevens = numBits - remainderBits;
         UInt u = 0;
         uint8_t byte;
         UInt shift = 0;
         do {
             if (!readFixedU8(&byte))
                 return false;
             if (!(byte & 0x80)) {
-                if (out)
-                    *out = u | UInt(byte) << shift;
+                *out = u | UInt(byte) << shift;
                 return true;
             }
             u |= UInt(byte & 0x7F) << shift;
             shift += 7;
         } while (shift != numBitsInSevens);
         if (!readFixedU8(&byte) || (byte & (unsigned(-1) << remainderBits)))
             return false;
-        if (out)
-            *out = u | UInt(byte) << numBitsInSevens;
+        *out = u | UInt(byte) << numBitsInSevens;
         return true;
     }
 
   public:
     Decoder(const uint8_t* begin, const uint8_t* end)
       : beg_(begin),
         end_(end),
         cur_(begin)
     {
         MOZ_ASSERT(begin <= end);
     }
-    explicit Decoder(const Bytecode& bytecode)
-      : beg_(bytecode.begin()),
-        end_(bytecode.end()),
-        cur_(bytecode.begin())
+    explicit Decoder(const Bytes& bytes)
+      : beg_(bytes.begin()),
+        end_(bytes.end()),
+        cur_(bytes.begin())
     {}
 
     bool done() const {
         MOZ_ASSERT(cur_ <= end_);
         return cur_ == end_;
     }
 
     uintptr_t bytesRemain() const {
@@ -615,71 +601,69 @@ class Decoder
         return uintptr_t(end_ - cur_);
     }
     const uint8_t* currentPosition() const {
         return cur_;
     }
     size_t currentOffset() const {
         return cur_ - beg_;
     }
-    void assertCurrentIs(const DebugOnly<size_t> offset) const {
-        MOZ_ASSERT(currentOffset() == offset);
-    }
 
     // Fixed-size encoding operations simply copy the literal bytes (without
     // attempting to align).
 
-    MOZ_WARN_UNUSED_RESULT bool readFixedU32(uint32_t* u = nullptr) {
+    MOZ_WARN_UNUSED_RESULT bool readFixedU32(uint32_t* u) {
         return read<uint32_t>(u);
     }
-    MOZ_WARN_UNUSED_RESULT bool readFixedF32(float* f = nullptr) {
+    MOZ_WARN_UNUSED_RESULT bool readFixedF32(float* f) {
         return read<float>(f);
     }
-    MOZ_WARN_UNUSED_RESULT bool readFixedF64(double* d = nullptr) {
+    MOZ_WARN_UNUSED_RESULT bool readFixedF64(double* d) {
         return read<double>(d);
     }
-    MOZ_WARN_UNUSED_RESULT bool readFixedI32x4(I32x4* i32x4 = nullptr) {
+    MOZ_WARN_UNUSED_RESULT bool readFixedI32x4(I32x4* i32x4) {
         return read<I32x4>(i32x4);
     }
-    MOZ_WARN_UNUSED_RESULT bool readFixedF32x4(F32x4* f32x4 = nullptr) {
+    MOZ_WARN_UNUSED_RESULT bool readFixedF32x4(F32x4* f32x4) {
         return read<F32x4>(f32x4);
     }
 
     // Variable-length encodings that all use LEB128.
 
-    MOZ_WARN_UNUSED_RESULT bool readVarU32(uint32_t* out = nullptr) {
+    MOZ_WARN_UNUSED_RESULT bool readVarU32(uint32_t* out) {
         return readVarU<uint32_t>(out);
     }
-    MOZ_WARN_UNUSED_RESULT bool readVarU64(uint64_t* out = nullptr) {
+    MOZ_WARN_UNUSED_RESULT bool readVarU64(uint64_t* out) {
         return readVarU<uint64_t>(out);
     }
-    MOZ_WARN_UNUSED_RESULT bool readExpr(Expr* expr = nullptr) {
+    MOZ_WARN_UNUSED_RESULT bool readExpr(Expr* expr) {
         return readEnum(expr);
     }
     MOZ_WARN_UNUSED_RESULT bool readValType(ValType* type) {
         return readEnum(type);
     }
     MOZ_WARN_UNUSED_RESULT bool readExprType(ExprType* type) {
         return readEnum(type);
     }
 
-    // C-strings are written in UTF8 and null-terminated while raw data can
-    // contain nulls and instead has an explicit byte length.
+    // See writeBytes comment.
 
-    MOZ_WARN_UNUSED_RESULT UniqueChars readCString() {
-        const char* begin = reinterpret_cast<const char*>(cur_);
-        for (; cur_ != end_; cur_++) {
-            if (!*cur_) {
-                cur_++;
-                return UniqueChars(DuplicateString(begin));
-            }
-        }
-        return nullptr;
+    MOZ_WARN_UNUSED_RESULT bool readBytes(Bytes* bytes) {
+        uint32_t numBytes;
+        if (!readVarU32(&numBytes))
+            return false;
+        if (bytesRemain() < numBytes)
+            return false;
+        if (!bytes->resize(numBytes))
+            return false;
+        memcpy(bytes->begin(), cur_, numBytes);
+        cur_ += numBytes;
+        return true;
     }
-    MOZ_WARN_UNUSED_RESULT bool readRawData(uint32_t numBytes, const uint8_t** bytes = nullptr) {
+    MOZ_WARN_UNUSED_RESULT bool readBytesRaw(uint32_t numBytes, const uint8_t** bytes) {
         if (bytes)
             *bytes = cur_;
         if (bytesRemain() < numBytes)
             return false;
         cur_ += numBytes;
         return true;
     }
 
@@ -701,17 +685,17 @@ class Decoder
         if (!readVarU32(&idSize))
             goto backup;
         if (bytesRemain() < idSize)
             return false;
         if (idSize != IdSize || !!memcmp(cur_, id, IdSize))
             goto backup;
         cur_ += IdSize;
         *startOffset = before - beg_;
-        return  true;
+        return true;
       backup:
         cur_ = before;
         *startOffset = NotStarted;
         return true;
     }
     MOZ_WARN_UNUSED_RESULT bool finishSection(uint32_t startOffset) {
         uint32_t currentOffset = cur_ - beg_;
         cur_ = beg_ + startOffset;
@@ -730,17 +714,17 @@ class Decoder
             return false;
         if (uint32_t(cur_ - begin) > size)
             return false;
         cur_ = begin + size;
         return true;
     }
 
     // The infallible "unchecked" decoding functions can be used when we are
-    // sure that the bytecode is well-formed (by construction or due to previous
+    // sure that the bytes are well-formed (by construction or due to previous
     // validation).
 
     uint32_t uncheckedReadFixedU32() {
         return uncheckedRead<uint32_t>();
     }
     float uncheckedReadFixedF32() {
         return uncheckedRead<float>();
     }
@@ -782,23 +766,31 @@ class Decoder
     }
     ValType uncheckedReadValType() {
         return uncheckedReadEnum<ValType>();
     }
 
     // Temporary encoding forms which should be removed as part of the
     // conversion to wasm:
 
-    MOZ_WARN_UNUSED_RESULT bool readFixedU8(uint8_t* i = nullptr) {
+    MOZ_WARN_UNUSED_RESULT bool readFixedU8(uint8_t* i) {
         return read<uint8_t>(i);
     }
-    MOZ_WARN_UNUSED_RESULT bool readFixedI32(int32_t* i = nullptr) {
-        return read<int32_t>(i);
-    }
     uint8_t uncheckedReadFixedU8() {
         return uncheckedRead<uint8_t>();
     }
 };
 
+// Reusable macro encoding/decoding functions reused by both the two
+// encoders (AsmJS/WasmText) and decoders (Wasm/WasmIonCompile).
+
+typedef Vector<ValType, 8, SystemAllocPolicy> ValTypeVector;
+
+bool
+EncodeLocalEntries(Encoder& d, const ValTypeVector& locals);
+
+bool
+DecodeLocalEntries(Decoder& d, ValTypeVector* locals);
+
 } // namespace wasm
 } // namespace js
 
 #endif // wasm_binary_h
--- a/js/src/asmjs/WasmGenerator.cpp
+++ b/js/src/asmjs/WasmGenerator.cpp
@@ -296,17 +296,17 @@ ModuleGenerator::convertOutOfRangeBranch
     masm_.clearJumpSites();
 
     return true;
 }
 
 bool
 ModuleGenerator::finishTask(IonCompileTask* task)
 {
-    const FuncBytecode& func = task->func();
+    const FuncBytes& func = task->func();
     FuncCompileResults& results = task->results();
 
     // Before merging in the new function's code, if jumps/calls in a previous
     // function's body might go out of range, patch these to thunks which have
     // full range.
     if ((masm_.size() - startOfUnpatchedBranches_) + results.masm().size() > JumpRange()) {
         startOfUnpatchedBranches_ = masm_.size();
         if (!convertOutOfRangeBranchesToThunks())
@@ -720,17 +720,17 @@ ModuleGenerator::numExports() const
 {
     return module_->exports.length();
 }
 
 bool
 ModuleGenerator::addMemoryExport(UniqueChars fieldName)
 {
     return exportMap_->fieldNames.append(Move(fieldName)) &&
-           exportMap_->fieldsToExports.append(ExportMap::MemoryExport);
+           exportMap_->fieldsToExports.append(MemoryExport);
 }
 
 bool
 ModuleGenerator::startFuncDefs()
 {
     MOZ_ASSERT(!startedFuncDefs());
     threadView_ = MakeUnique<ModuleGeneratorThreadView>(*shared_);
     if (!threadView_)
@@ -777,44 +777,36 @@ ModuleGenerator::startFuncDef(uint32_t l
     MOZ_ASSERT(!activeFunc_);
     MOZ_ASSERT(!finishedFuncs_);
 
     if (freeTasks_.empty() && !finishOutstandingTask())
         return false;
 
     IonCompileTask* task = freeTasks_.popCopy();
 
-    task->reset(&fg->bytecode_);
-    if (fg->bytecode_) {
-        fg->bytecode_->clear();
-    } else {
-        fg->bytecode_ = MakeUnique<Bytecode>();
-        if (!fg->bytecode_)
-            return false;
-    }
-
+    task->reset(&fg->bytes_);
+    fg->bytes_.clear();
     fg->lineOrBytecode_ = lineOrBytecode;
     fg->m_ = this;
     fg->task_ = task;
     activeFunc_ = fg;
     return true;
 }
 
 bool
 ModuleGenerator::finishFuncDef(uint32_t funcIndex, unsigned generateTime, FunctionGenerator* fg)
 {
     MOZ_ASSERT(activeFunc_ == fg);
 
-    UniqueFuncBytecode func =
-        js::MakeUnique<FuncBytecode>(funcIndex,
-                                     funcSig(funcIndex),
-                                     Move(fg->bytecode_),
-                                     fg->lineOrBytecode_,
-                                     Move(fg->callSiteLineNums_),
-                                     generateTime);
+    auto func = js::MakeUnique<FuncBytes>(Move(fg->bytes_),
+                                          funcIndex,
+                                          funcSig(funcIndex),
+                                          fg->lineOrBytecode_,
+                                          Move(fg->callSiteLineNums_),
+                                          generateTime);
     if (!func)
         return false;
 
     fg->task_->init(Move(func));
 
     if (parallel_) {
         if (!StartOffThreadWasmCompile(cx_, fg->task_))
             return false;
--- a/js/src/asmjs/WasmGenerator.h
+++ b/js/src/asmjs/WasmGenerator.h
@@ -46,17 +46,17 @@ struct SlowFunction
 };
 typedef Vector<SlowFunction> SlowFunctionVector;
 
 // The ModuleGeneratorData holds all the state shared between the
 // ModuleGenerator and ModuleGeneratorThreadView. The ModuleGeneratorData
 // is encapsulated by ModuleGenerator/ModuleGeneratorThreadView classes which
 // present a race-free interface to the code in each thread assuming any given
 // element is initialized by the ModuleGenerator thread before an index to that
-// element is written to Bytecode sent to a ModuleGeneratorThreadView thread.
+// element is written to Bytes sent to a ModuleGeneratorThreadView thread.
 // Once created, the Vectors are never resized.
 
 struct TableModuleGeneratorData
 {
     uint32_t globalDataOffset;
     uint32_t numElems;
     Uint32Vector elemFuncIndices;
 
@@ -197,18 +197,18 @@ class MOZ_STACK_CLASS ModuleGenerator
     // Parallel compilation
     bool                            parallel_;
     uint32_t                        outstanding_;
     UniqueModuleGeneratorThreadView threadView_;
     Vector<IonCompileTask>          tasks_;
     Vector<IonCompileTask*>         freeTasks_;
 
     // Assertions
-    DebugOnly<FunctionGenerator*>   activeFunc_;
-    DebugOnly<bool>                 finishedFuncs_;
+    FunctionGenerator*              activeFunc_;
+    bool                            finishedFuncs_;
 
     bool finishOutstandingTask();
     bool funcIsDefined(uint32_t funcIndex) const;
     uint32_t funcEntry(uint32_t funcIndex) const;
     bool convertOutOfRangeBranchesToThunks();
     bool finishTask(IonCompileTask* task);
     bool finishCodegen(StaticLinkData* link);
     bool finishStaticLinkData(uint8_t* code, uint32_t codeBytes, StaticLinkData* link);
@@ -283,33 +283,33 @@ class MOZ_STACK_CLASS ModuleGenerator
 // anything else. After the body is complete, ModuleGenerator::finishFunc must
 // be called before the FunctionGenerator is destroyed and the next function is
 // started.
 
 class MOZ_STACK_CLASS FunctionGenerator
 {
     friend class ModuleGenerator;
 
-    ModuleGenerator*   m_;
-    IonCompileTask*    task_;
+    ModuleGenerator* m_;
+    IonCompileTask*  task_;
 
     // Data created during function generation, then handed over to the
-    // FuncBytecode in ModuleGenerator::finishFunc().
-    UniqueBytecode     bytecode_;
-    Uint32Vector       callSiteLineNums_;
+    // FuncBytes in ModuleGenerator::finishFunc().
+    Bytes            bytes_;
+    Uint32Vector     callSiteLineNums_;
 
     uint32_t lineOrBytecode_;
 
   public:
     FunctionGenerator()
       : m_(nullptr), task_(nullptr), lineOrBytecode_(0)
     {}
 
-    Bytecode& bytecode() const {
-        return *bytecode_;
+    Bytes& bytes() {
+        return bytes_;
     }
     bool addCallSiteLineNum(uint32_t lineno) {
         return callSiteLineNums_.append(lineno);
     }
 };
 
 } // namespace wasm
 } // namespace js
--- a/js/src/asmjs/WasmIonCompile.cpp
+++ b/js/src/asmjs/WasmIonCompile.cpp
@@ -35,17 +35,17 @@ typedef Vector<MBasicBlock*, 8, SystemAl
 // MIR graph.
 class FunctionCompiler
 {
   private:
     typedef Vector<BlockVector, 0, SystemAllocPolicy> BlocksVector;
 
     ModuleGeneratorThreadView& mg_;
     Decoder&                   decoder_;
-    const FuncBytecode&        func_;
+    const FuncBytes&           func_;
     const ValTypeVector&       locals_;
     size_t                     lastReadCallSite_;
 
     TempAllocator&             alloc_;
     MIRGraph&                  graph_;
     const CompileInfo&         info_;
     MIRGenerator&              mirGen_;
 
@@ -55,17 +55,17 @@ class FunctionCompiler
     uint32_t                   blockDepth_;
     BlocksVector               targets_;
 
     FuncCompileResults&        compileResults_;
 
   public:
     FunctionCompiler(ModuleGeneratorThreadView& mg,
                      Decoder& decoder,
-                     const FuncBytecode& func,
+                     const FuncBytes& func,
                      const ValTypeVector& locals,
                      MIRGenerator& mirGen,
                      FuncCompileResults& compileResults)
       : mg_(mg),
         decoder_(decoder),
         func_(func),
         locals_(locals),
         lastReadCallSite_(0),
@@ -148,17 +148,17 @@ class FunctionCompiler
         MOZ_ASSERT(loopDepth_ == 0);
         MOZ_ASSERT(blockDepth_ == 0);
 #ifdef DEBUG
         for (BlockVector& vec : targets_) {
             MOZ_ASSERT(vec.empty());
         }
 #endif
         MOZ_ASSERT(inDeadCode());
-        MOZ_ASSERT(decoder_.done(), "all bytecode must be consumed");
+        MOZ_ASSERT(decoder_.done(), "all bytes must be consumed");
         MOZ_ASSERT(func_.callSiteLineNums().length() == lastReadCallSite_);
     }
 
     /************************* Read-only interface (after local scope setup) */
 
     MIRGenerator&       mirGen() const     { return mirGen_; }
     MIRGraph&           mirGraph() const   { return graph_; }
     const CompileInfo&  info() const       { return info_; }
@@ -3043,32 +3043,28 @@ EmitExpr(FunctionCompiler& f, MDefinitio
     MOZ_CRASH("unexpected wasm opcode");
 }
 
 bool
 wasm::IonCompileFunction(IonCompileTask* task)
 {
     int64_t before = PRMJ_Now();
 
-    const FuncBytecode& func = task->func();
+    const FuncBytes& func = task->func();
     FuncCompileResults& results = task->results();
 
-    // Read in the variable types to build the local types vector.
-
-    Decoder d(func.bytecode());
+    Decoder d(func.bytes());
+
+    // Build the local types vector.
 
     ValTypeVector locals;
     if (!locals.appendAll(func.sig().args()))
         return false;
-
-    uint32_t numVars = d.uncheckedReadVarU32();
-    for (uint32_t i = 0; i < numVars; i++) {
-        if (!locals.append(d.uncheckedReadValType()))
-            return false;
-    }
+    if (!DecodeLocalEntries(d, &locals))
+        return false;
 
     // Set up for Ion compilation.
 
     JitContext jitContext(CompileRuntime::get(task->runtime()), &results.alloc());
     const JitCompileOptions options;
     MIRGraph graph(&results.alloc());
     CompileInfo compileInfo(locals.length());
     MIRGenerator mir(nullptr, options, &results.alloc(), &graph, &compileInfo,
--- a/js/src/asmjs/WasmIonCompile.h
+++ b/js/src/asmjs/WasmIonCompile.h
@@ -26,62 +26,58 @@ namespace js {
 namespace wasm {
 
 class ModuleGeneratorThreadView;
 
 typedef Vector<jit::MIRType, 8, SystemAllocPolicy> MIRTypeVector;
 typedef jit::ABIArgIter<MIRTypeVector> ABIArgMIRTypeIter;
 typedef jit::ABIArgIter<ValTypeVector> ABIArgValTypeIter;
 
-// The FuncBytecode class contains the intermediate representation of a
-// parsed/decoded and validated asm.js/WebAssembly function. The FuncBytecode
-// lives only until it is fully compiled.
+// The FuncBytes class represents a single, concurrently-compilable function.
+// A FuncBytes object is composed of the wasm function body bytes along with the
+// ambient metadata describing the function necessary to compile it.
 
-class FuncBytecode
+class FuncBytes
 {
-    // Function metadata
+    Bytes              bytes_;
+    uint32_t           index_;
     const DeclaredSig& sig_;
-    uint32_t lineOrBytecode_;
-    Uint32Vector callSiteLineNums_;
-
-    // Compilation bookkeeping
-    uint32_t index_;
-    unsigned generateTime_;
-
-    UniqueBytecode bytecode_;
+    uint32_t           lineOrBytecode_;
+    Uint32Vector       callSiteLineNums_;
+    unsigned           generateTime_;
 
   public:
-    FuncBytecode(uint32_t index,
-                 const DeclaredSig& sig,
-                 UniqueBytecode bytecode,
-                 uint32_t lineOrBytecode,
-                 Uint32Vector&& callSiteLineNums,
-                 unsigned generateTime)
-      : sig_(sig),
+    FuncBytes(Bytes&& bytes,
+              uint32_t index,
+              const DeclaredSig& sig,
+              uint32_t lineOrBytecode,
+              Uint32Vector&& callSiteLineNums,
+              unsigned generateTime)
+      : bytes_(Move(bytes)),
+        index_(index),
+        sig_(sig),
         lineOrBytecode_(lineOrBytecode),
         callSiteLineNums_(Move(callSiteLineNums)),
-        index_(index),
-        generateTime_(generateTime),
-        bytecode_(Move(bytecode))
+        generateTime_(generateTime)
     {}
 
-    UniqueBytecode recycleBytecode() { return Move(bytecode_); }
-
+    Bytes& bytes() { return bytes_; }
+    const Bytes& bytes() const { return bytes_; }
+    uint32_t index() const { return index_; }
+    const DeclaredSig& sig() const { return sig_; }
     uint32_t lineOrBytecode() const { return lineOrBytecode_; }
     const Uint32Vector& callSiteLineNums() const { return callSiteLineNums_; }
-    uint32_t index() const { return index_; }
-    const DeclaredSig& sig() const { return sig_; }
-    const Bytecode& bytecode() const { return *bytecode_; }
     unsigned generateTime() const { return generateTime_; }
 };
 
-typedef UniquePtr<FuncBytecode> UniqueFuncBytecode;
+typedef UniquePtr<FuncBytes> UniqueFuncBytes;
 
-// The FuncCompileResults contains the results of compiling a single function
-// body, ready to be merged into the whole-module MacroAssembler.
+// The FuncCompileResults class contains the results of compiling a single
+// function body, ready to be merged into the whole-module MacroAssembler.
+
 class FuncCompileResults
 {
     jit::TempAllocator alloc_;
     jit::MacroAssembler masm_;
     FuncOffsets offsets_;
     unsigned compileTime_;
 
     FuncCompileResults(const FuncCompileResults&) = delete;
@@ -103,23 +99,24 @@ class FuncCompileResults
 };
 
 // An IonCompileTask represents the task of compiling a single function body. An
 // IonCompileTask is filled with the wasm code to be compiled on the main
 // validation thread, sent off to an Ion compilation helper thread which creates
 // the FuncCompileResults, and finally sent back to the validation thread. To
 // save time allocating and freeing memory, IonCompileTasks are reset() and
 // reused.
+
 class IonCompileTask
 {
-    JSRuntime* const runtime_;
+    JSRuntime* const           runtime_;
     ModuleGeneratorThreadView& mg_;
-    LifoAlloc lifo_;
-    UniqueFuncBytecode func_;
-    mozilla::Maybe<FuncCompileResults> results_;
+    LifoAlloc                  lifo_;
+    UniqueFuncBytes            func_;
+    Maybe<FuncCompileResults>  results_;
 
     IonCompileTask(const IonCompileTask&) = delete;
     IonCompileTask& operator=(const IonCompileTask&) = delete;
 
   public:
     IonCompileTask(JSRuntime* rt, ModuleGeneratorThreadView& mg, size_t defaultChunkSize)
       : runtime_(rt), mg_(mg), lifo_(defaultChunkSize), func_(nullptr)
     {}
@@ -127,31 +124,31 @@ class IonCompileTask
         return runtime_;
     }
     LifoAlloc& lifo() {
         return lifo_;
     }
     ModuleGeneratorThreadView& mg() const {
         return mg_;
     }
-    void init(UniqueFuncBytecode func) {
+    void init(UniqueFuncBytes func) {
         MOZ_ASSERT(!func_);
-        func_ = mozilla::Move(func);
+        func_ = Move(func);
         results_.emplace(lifo_);
     }
-    const FuncBytecode& func() const {
+    const FuncBytes& func() const {
         MOZ_ASSERT(func_);
         return *func_;
     }
     FuncCompileResults& results() {
         return *results_;
     }
-    void reset(UniqueBytecode* recycled) {
+    void reset(Bytes* recycled) {
         if (func_)
-            *recycled = func_->recycleBytecode();
+            *recycled = Move(func_->bytes());
         func_.reset(nullptr);
         results_.reset();
         lifo_.releaseAll();
     }
 };
 
 bool
 IonCompileFunction(IonCompileTask* task);
--- a/js/src/asmjs/WasmModule.cpp
+++ b/js/src/asmjs/WasmModule.cpp
@@ -49,18 +49,16 @@ using namespace js::jit;
 using namespace js::wasm;
 using mozilla::BinarySearch;
 using mozilla::MakeEnumeratedRange;
 using mozilla::PodCopy;
 using mozilla::PodZero;
 using mozilla::Swap;
 using JS::GenericNaN;
 
-const uint32_t ExportMap::MemoryExport;
-
 UniqueCodePtr
 wasm::AllocateCode(ExclusiveContext* cx, size_t bytes)
 {
     // On most platforms, this will allocate RWX memory. On iOS, or when
     // --non-writable-jitcode is used, this will allocate RW memory. In this
     // case, DynamicallyLinkModule will reprotect the code as RX.
     unsigned permissions =
         ExecutableAllocator::initialProtectionFlags(ExecutableAllocator::Writable);
@@ -1125,17 +1123,17 @@ CreateExportObject(JSContext* cx,
     MOZ_ASSERT(exportMap.exportFuncIndices.length() == exports.length());
     MOZ_ASSERT(exportMap.fieldNames.length() == exportMap.fieldsToExports.length());
 
     for (size_t fieldIndex = 0; fieldIndex < exportMap.fieldNames.length(); fieldIndex++) {
         const char* fieldName = exportMap.fieldNames[fieldIndex].get();
         if (!*fieldName) {
             MOZ_ASSERT(!exportObj);
             uint32_t exportIndex = exportMap.fieldsToExports[fieldIndex];
-            if (exportIndex == ExportMap::MemoryExport) {
+            if (exportIndex == MemoryExport) {
                 MOZ_ASSERT(heap);
                 exportObj.set(heap);
             } else {
                 exportObj.set(NewExportedFunction(cx, moduleObj, exportMap, exportIndex));
                 if (!exportObj)
                     return false;
             }
             break;
@@ -1162,17 +1160,17 @@ CreateExportObject(JSContext* cx,
 
         JSAtom* atom = AtomizeUTF8Chars(cx, fieldName, strlen(fieldName));
         if (!atom)
             return false;
 
         RootedId id(cx, AtomToId(atom));
         RootedValue val(cx);
         uint32_t exportIndex = exportMap.fieldsToExports[fieldIndex];
-        if (exportIndex == ExportMap::MemoryExport)
+        if (exportIndex == MemoryExport)
             val = ObjectValue(*heap);
         else
             val = vals[exportIndex];
 
         if (!JS_DefinePropertyById(cx, exportObj, id, val, JSPROP_ENUMERATE))
             return false;
     }
 
--- a/js/src/asmjs/WasmModule.h
+++ b/js/src/asmjs/WasmModule.h
@@ -331,20 +331,20 @@ typedef Vector<CacheableChars, 0, System
 // The 'fieldNames' vector provides the list of names of the module's exports.
 // For each field in fieldNames, 'fieldsToExports' provides either:
 //  - the sentinel value MemoryExport indicating an export of linear memory; or
 //  - the index of an export (both into the module's ExportVector and the
 //    ExportMap's exportFuncIndices vector).
 // Lastly, the 'exportFuncIndices' vector provides, for each exported function,
 // the internal index of the function.
 
+static const uint32_t MemoryExport = UINT32_MAX;
+
 struct ExportMap
 {
-    static const uint32_t MemoryExport = UINT32_MAX;
-
     CacheableCharsVector fieldNames;
     Uint32Vector fieldsToExports;
     Uint32Vector exportFuncIndices;
 
     WASM_DECLARE_SERIALIZABLE(ExportMap)
 };
 
 typedef UniquePtr<ExportMap> UniqueExportMap;
--- a/js/src/asmjs/WasmText.cpp
+++ b/js/src/asmjs/WasmText.cpp
@@ -779,16 +779,17 @@ class WasmToken
         GetLocal,
         If,
         IfElse,
         Import,
         Index,
         UnsignedInteger,
         SignedInteger,
         Memory,
+        NegativeZero,
         Load,
         Local,
         Loop,
         Module,
         Name,
         Nop,
         Offset,
         OpenParen,
@@ -1266,16 +1267,18 @@ WasmTokenStream::literal(const char16_t*
             if (!u.isValid())
                 return LexHexFloatLiteral(begin, end_, &cur_);
 
             cur_++;
         } while (cur_ != end_);
 
         if (*begin == '-') {
             uint64_t value = u.value();
+            if (value == 0)
+                return WasmToken(WasmToken::NegativeZero, begin, cur_);
             if (value > uint64_t(INT64_MIN))
                 return LexHexFloatLiteral(begin, end_, &cur_);
 
             value = -value;
             return WasmToken(int64_t(value), begin, cur_);
         }
     } else {
         while (cur_ != end_) {
@@ -1290,16 +1293,18 @@ WasmTokenStream::literal(const char16_t*
             if (!u.isValid())
                 return LexDecFloatLiteral(begin, end_, &cur_);
 
             cur_++;
         }
 
         if (*begin == '-') {
             uint64_t value = u.value();
+            if (value == 0)
+                return WasmToken(WasmToken::NegativeZero, begin, cur_);
             if (value > uint64_t(INT64_MIN))
                 return LexDecFloatLiteral(begin, end_, &cur_);
 
             value = -value;
             return WasmToken(int64_t(value), begin, cur_);
         }
     }
 
@@ -2265,16 +2270,19 @@ ParseFloatLiteral(WasmParseContext& c, W
         *result = token.index();
         return true;
       case WasmToken::UnsignedInteger:
         *result = token.uint();
         return true;
       case WasmToken::SignedInteger:
         *result = token.sint();
         return true;
+      case WasmToken::NegativeZero:
+        *result = -0.0;
+        return true;
       case WasmToken::Float:
         break;
       default:
         c.ts.generateError(token, c.error);
         return false;
     }
 
     const char16_t* begin = token.begin();
@@ -2340,29 +2348,33 @@ ParseConst(WasmParseContext& c, WasmToke
           case WasmToken::Index:
             return new(c.lifo) WasmAstConst(Val(val.index()));
           case WasmToken::SignedInteger: {
             CheckedInt<int32_t> sint = val.sint();
             if (!sint.isValid())
                 break;
             return new(c.lifo) WasmAstConst(Val(uint32_t(sint.value())));
           }
+          case WasmToken::NegativeZero:
+            return new(c.lifo) WasmAstConst(Val(uint32_t(0)));
           default:
             break;
         }
         break;
       }
       case ValType::I64: {
         switch (val.kind()) {
           case WasmToken::Index:
             return new(c.lifo) WasmAstConst(Val(uint64_t(val.index())));
           case WasmToken::UnsignedInteger:
             return new(c.lifo) WasmAstConst(Val(val.uint()));
           case WasmToken::SignedInteger:
             return new(c.lifo) WasmAstConst(Val(uint64_t(val.sint())));
+          case WasmToken::NegativeZero:
+            return new(c.lifo) WasmAstConst(Val(uint32_t(0)));
           default:
             break;
         }
         break;
       }
       case ValType::F32: {
         float result;
         if (!ParseFloatLiteral(c, val, &result))
@@ -3765,33 +3777,33 @@ EncodeFunctionSignatures(Encoder& e, Was
             return false;
     }
 
     e.finishSection(offset);
     return true;
 }
 
 static bool
-EncodeCString(Encoder& e, WasmName wasmName)
+EncodeBytes(Encoder& e, WasmName wasmName)
 {
     TwoByteChars range(wasmName.begin(), wasmName.length());
     UniqueChars utf8(JS::CharsToNewUTF8CharsZ(nullptr, range).c_str());
-    return utf8 && e.writeCString(utf8.get());
+    return utf8 && e.writeBytes(utf8.get(), strlen(utf8.get()));
 }
 
 static bool
 EncodeImport(Encoder& e, WasmAstImport& imp)
 {
     if (!e.writeVarU32(imp.sigIndex()))
         return false;
 
-    if (!EncodeCString(e, imp.module()))
+    if (!EncodeBytes(e, imp.module()))
         return false;
 
-    if (!EncodeCString(e, imp.func()))
+    if (!EncodeBytes(e, imp.func()))
         return false;
 
     return true;
 }
 
 static bool
 EncodeImportTable(Encoder& e, WasmAstModule& module)
 {
@@ -3849,17 +3861,17 @@ EncodeMemory(Encoder& e, WasmAstModule& 
 }
 
 static bool
 EncodeFunctionExport(Encoder& e, WasmAstExport& exp)
 {
     if (!e.writeVarU32(exp.func().index()))
         return false;
 
-    if (!EncodeCString(e, exp.name()))
+    if (!EncodeBytes(e, exp.name()))
         return false;
 
     return true;
 }
 
 static bool
 EncodeExportTable(Encoder& e, WasmAstModule& module)
 {
@@ -3920,23 +3932,21 @@ static bool
 EncodeFunctionBody(Encoder& e, WasmAstFunc& func)
 {
     size_t bodySizeAt;
     if (!e.writePatchableVarU32(&bodySizeAt))
         return false;
 
     size_t beforeBody = e.currentOffset();
 
-    if (!e.writeVarU32(func.vars().length()))
+    ValTypeVector varTypes;
+    if (!varTypes.appendAll(func.vars()))
         return false;
-
-    for (ValType type : func.vars()) {
-        if (!e.writeValType(type))
-            return false;
-    }
+    if (!EncodeLocalEntries(e, varTypes))
+        return false;
 
     for (WasmAstExpr* expr : func.body()) {
         if (!EncodeExpr(e, *expr))
             return false;
     }
 
     e.patchVarU32(bodySizeAt, e.currentOffset() - beforeBody);
     return true;
@@ -3976,20 +3986,17 @@ EncodeDataSegment(Encoder& e, WasmAstSeg
     const char16_t* cur = text.begin();
     const char16_t* end = text.end();
     while (cur != end) {
         uint8_t byte;
         MOZ_ALWAYS_TRUE(ConsumeTextByte(&cur, end, &byte));
         bytes.infallibleAppend(byte);
     }
 
-    if (!e.writeVarU32(bytes.length()))
-        return false;
-
-    if (!e.writeRawData(bytes.begin(), bytes.length()))
+    if (!e.writeBytes(bytes.begin(), bytes.length()))
         return false;
 
     return true;
 }
 
 static bool
 EncodeDataSegments(Encoder& e, WasmAstModule& module)
 {
@@ -4009,65 +4016,61 @@ EncodeDataSegments(Encoder& e, WasmAstMo
         if (!EncodeDataSegment(e, *segment))
             return false;
     }
 
     e.finishSection(offset);
     return true;
 }
 
-static UniqueBytecode
-EncodeModule(WasmAstModule& module)
+static bool
+EncodeModule(WasmAstModule& module, Bytes* bytes)
 {
-    UniqueBytecode bytecode = MakeUnique<Bytecode>();
-    if (!bytecode)
-        return nullptr;
-
-    Encoder e(*bytecode);
+    Encoder e(*bytes);
 
     if (!e.writeFixedU32(MagicNumber))
-        return nullptr;
+        return false;
 
     if (!e.writeFixedU32(EncodingVersion))
-        return nullptr;
+        return false;
 
     if (!EncodeSignatures(e, module))
-        return nullptr;
+        return false;
 
     if (!EncodeImportTable(e, module))
-        return nullptr;
+        return false;
 
     if (!EncodeFunctionSignatures(e, module))
-        return nullptr;
+        return false;
 
     if (!EncodeFunctionTable(e, module))
-        return nullptr;
+        return false;
 
     if (!EncodeMemory(e, module))
-        return nullptr;
+        return false;
 
     if (!EncodeExportTable(e, module))
-        return nullptr;
+        return false;
 
     if (!EncodeFunctionBodies(e, module))
-        return nullptr;
+        return false;
 
     if (!EncodeDataSegments(e, module))
-        return nullptr;
-
-    return Move(bytecode);
+        return false;
+
+    return true;
 }
 
 /*****************************************************************************/
 
-UniqueBytecode
-wasm::TextToBinary(const char16_t* text, UniqueChars* error)
+bool
+wasm::TextToBinary(const char16_t* text, Bytes* bytes, UniqueChars* error)
 {
     LifoAlloc lifo(AST_LIFO_DEFAULT_CHUNK_SIZE);
     WasmAstModule* module = ParseModule(text, lifo, error);
     if (!module)
-        return nullptr;
+        return false;
 
     if (!ResolveModule(lifo, module, error))
-        return nullptr;
-
-    return EncodeModule(*module);
+        return false;
+
+    return EncodeModule(*module, bytes);
 }
--- a/js/src/asmjs/WasmText.h
+++ b/js/src/asmjs/WasmText.h
@@ -21,18 +21,18 @@
 
 #include "asmjs/WasmBinary.h"
 #include "js/Utility.h"
 
 namespace js {
 namespace wasm {
 
 // Translate the textual representation of a wasm module (given by a
-// null-terminated char16_t array) into a Bytecode object. If there is an error
+// null-terminated char16_t array) into serialized bytes. If there is an error
 // other than out-of-memory an error message string will be stored in 'error'.
 
-extern UniqueBytecode
-TextToBinary(const char16_t* text, UniqueChars* error);
+extern bool
+TextToBinary(const char16_t* text, Bytes* bytes, UniqueChars* error);
 
 } // namespace wasm
 } // namespace js
 
 #endif // wasm_text_h
--- a/js/src/asmjs/WasmTypes.h
+++ b/js/src/asmjs/WasmTypes.h
@@ -16,16 +16,17 @@
  * limitations under the License.
  */
 
 #ifndef wasm_types_h
 #define wasm_types_h
 
 #include "mozilla/EnumeratedArray.h"
 #include "mozilla/HashFunctions.h"
+#include "mozilla/Maybe.h"
 #include "mozilla/Move.h"
 
 #include "NamespaceImports.h"
 
 #include "asmjs/WasmBinary.h"
 #include "ds/LifoAlloc.h"
 #include "jit/IonTypes.h"
 #include "js/UniquePtr.h"
@@ -34,21 +35,21 @@
 
 namespace js {
 
 class PropertyName;
 
 namespace wasm {
 
 using mozilla::EnumeratedArray;
+using mozilla::Maybe;
 using mozilla::Move;
 using mozilla::MallocSizeOf;
 
 typedef Vector<uint32_t, 0, SystemAllocPolicy> Uint32Vector;
-typedef Vector<ValType, 8, SystemAllocPolicy> ValTypeVector;
 
 // ValType/ExprType utilities
 
 static inline bool
 IsVoid(ExprType et)
 {
     return et == ExprType::Void;
 }
@@ -589,20 +590,21 @@ enum ModuleKind
 // Constants:
 
 static const unsigned ActivationGlobalDataOffset = 0;
 static const unsigned HeapGlobalDataOffset       = ActivationGlobalDataOffset + sizeof(void*);
 static const unsigned NaN64GlobalDataOffset      = HeapGlobalDataOffset + sizeof(void*);
 static const unsigned NaN32GlobalDataOffset      = NaN64GlobalDataOffset + sizeof(double);
 static const unsigned InitialGlobalDataBytes     = NaN32GlobalDataOffset + sizeof(float);
 
-static const unsigned MaxSigs                    =   4 * 1024;
-static const unsigned MaxFuncs                   = 512 * 1024;
-static const unsigned MaxImports                 =   4 * 1024;
-static const unsigned MaxExports                 =   4 * 1024;
-static const unsigned MaxTableElems              = 128 * 1024;
-static const unsigned MaxArgsPerFunc             =   4 * 1024;
-static const unsigned MaxBrTableElems            =   4 * 1024;
+static const unsigned MaxSigs                    =        4 * 1024;
+static const unsigned MaxFuncs                   =      512 * 1024;
+static const unsigned MaxLocals                  =       64 * 1024;
+static const unsigned MaxImports                 =       64 * 1024;
+static const unsigned MaxExports                 =       64 * 1024;
+static const unsigned MaxTableElems              =      128 * 1024;
+static const unsigned MaxArgsPerFunc             =        4 * 1024;
+static const unsigned MaxBrTableElems            = 4 * 1024 * 1024;
 
 } // namespace wasm
 } // namespace js
 
 #endif // wasm_types_h
--- a/js/src/builtin/Object.cpp
+++ b/js/src/builtin/Object.cpp
@@ -673,24 +673,144 @@ js::obj_getOwnPropertyDescriptor(JSConte
         return false;
 
     // Steps 5-7.
     Rooted<PropertyDescriptor> desc(cx);
     return GetOwnPropertyDescriptor(cx, obj, id, &desc) &&
            FromPropertyDescriptor(cx, desc, args.rval());
 }
 
-// ES6 draft rev27 (2014/08/24) 19.1.2.14 Object.keys(O)
+enum EnumerableOwnPropertiesKind {
+    Keys,
+    Values,
+    KeysAndValues
+};
+
+// ES7 proposal 2015-12-14
+// http://tc39.github.io/proposal-object-values-entries/#EnumerableOwnProperties
+static bool
+EnumerableOwnProperties(JSContext* cx, const JS::CallArgs& args, EnumerableOwnPropertiesKind kind)
+{
+    // Step 1. (Step 1 of Object.{keys,values,entries}, really.)
+    RootedObject obj(cx, ToObject(cx, args.get(0)));
+    if (!obj)
+        return false;
+
+    // Step 2.
+    AutoIdVector ids(cx);
+    if (!GetPropertyKeys(cx, obj, JSITER_OWNONLY | JSITER_HIDDEN, &ids))
+        return false;
+
+    // Step 3.
+    AutoValueVector properties(cx);
+    size_t len = ids.length();
+    if (!properties.resize(len))
+        return false;
+
+    RootedId id(cx);
+    RootedValue key(cx);
+    RootedValue value(cx);
+    RootedNativeObject nobj(cx);
+    if (obj->is<NativeObject>())
+        nobj = &obj->as<NativeObject>();
+    RootedShape shape(cx);
+    Rooted<PropertyDescriptor> desc(cx);
+
+    // Step 4.
+    size_t out = 0;
+    for (size_t i = 0; i < len; i++) {
+        id = ids[i];
+
+        // Step 4.a. (Symbols were filtered out in step 2.)
+        MOZ_ASSERT(!JSID_IS_SYMBOL(id));
+
+        if (kind != Values) {
+            if (!IdToStringOrSymbol(cx, id, &key))
+                return false;
+        }
+
+        // Step 4.a.i.
+        if (nobj) {
+            if (JSID_IS_INT(id) && nobj->containsDenseElement(JSID_TO_INT(id))) {
+                value = nobj->getDenseOrTypedArrayElement(JSID_TO_INT(id));
+            } else {
+                shape = nobj->lookup(cx, id);
+                if (!shape || !(GetShapeAttributes(nobj, shape) & JSPROP_ENUMERATE))
+                    continue;
+                if (!shape->isAccessorShape()) {
+                    if (!NativeGetExistingProperty(cx, nobj, nobj, shape, &value))
+                        return false;
+                } else if (!GetProperty(cx, obj, obj, id, &value)) {
+                    return false;
+                }
+            }
+        } else {
+            if (!GetOwnPropertyDescriptor(cx, obj, id, &desc))
+                return false;
+
+            // Step 4.a.ii. (inverted.)
+            if (!desc.object() || !desc.enumerable())
+                continue;
+
+            // Step 4.a.ii.1.
+            // (Omitted because Object.keys doesn't use this implementation.)
+
+            // Step 4.a.ii.2.a.
+            if (obj->isNative() && desc.hasValue())
+                value = desc.value();
+            else if (!GetProperty(cx, obj, obj, id, &value))
+                return false;
+        }
+
+        // Steps 4.a.ii.2.b-c.
+        if (kind == Values)
+            properties[out++].set(value);
+        else if (!NewValuePair(cx, key, value, properties[out++]))
+            return false;
+    }
+
+    // Step 5.
+    // (Implemented in step 2.)
+
+    // Step 3 of Object.{keys,values,entries}
+    JSObject* aobj = NewDenseCopiedArray(cx, out, properties.begin());
+    if (!aobj)
+        return false;
+
+    args.rval().setObject(*aobj);
+    return true;
+}
+
+// ES7 proposal 2015-12-14
+// http://tc39.github.io/proposal-object-values-entries/#Object.keys
 static bool
 obj_keys(JSContext* cx, unsigned argc, Value* vp)
 {
     CallArgs args = CallArgsFromVp(argc, vp);
     return GetOwnPropertyKeys(cx, args, JSITER_OWNONLY);
 }
 
+// ES7 proposal 2015-12-14
+// http://tc39.github.io/proposal-object-values-entries/#Object.values
+static bool
+obj_values(JSContext* cx, unsigned argc, Value* vp)
+{
+    CallArgs args = CallArgsFromVp(argc, vp);
+    return EnumerableOwnProperties(cx, args, Values);
+}
+
+// ES7 proposal 2015-12-14
+// http://tc39.github.io/proposal-object-values-entries/#Object.entries
+static bool
+obj_entries(JSContext* cx, unsigned argc, Value* vp)
+{
+    CallArgs args = CallArgsFromVp(argc, vp);
+    return EnumerableOwnProperties(cx, args, KeysAndValues);
+}
+
 /* ES6 draft 15.2.3.16 */
 static bool
 obj_is(JSContext* cx, unsigned argc, Value* vp)
 {
     CallArgs args = CallArgsFromVp(argc, vp);
 
     bool same;
     if (!SameValue(cx, args.get(0), args.get(1), &same))
@@ -971,58 +1091,56 @@ ProtoSetter(JSContext* cx, unsigned argc
     return true;
 }
 
 static const JSFunctionSpec object_methods[] = {
 #if JS_HAS_TOSOURCE
     JS_FN(js_toSource_str,             obj_toSource,                0,0),
 #endif
     JS_FN(js_toString_str,             obj_toString,                0,0),
-    JS_SELF_HOSTED_FN(js_toLocaleString_str, "Object_toLocaleString", 0,JSPROP_DEFINE_LATE),
+    JS_SELF_HOSTED_FN(js_toLocaleString_str, "Object_toLocaleString", 0, 0),
     JS_FN(js_valueOf_str,              obj_valueOf,                 0,0),
 #if JS_HAS_OBJ_WATCHPOINT
     JS_FN(js_watch_str,                obj_watch,                   2,0),
     JS_FN(js_unwatch_str,              obj_unwatch,                 1,0),
 #endif
     JS_FN(js_hasOwnProperty_str,       obj_hasOwnProperty,          1,0),
     JS_FN(js_isPrototypeOf_str,        obj_isPrototypeOf,           1,0),
     JS_FN(js_propertyIsEnumerable_str, obj_propertyIsEnumerable,    1,0),
 #if JS_OLD_GETTER_SETTER_METHODS
-    JS_SELF_HOSTED_FN(js_defineGetter_str, "ObjectDefineGetter",    2,JSPROP_DEFINE_LATE),
-    JS_SELF_HOSTED_FN(js_defineSetter_str, "ObjectDefineSetter",    2,JSPROP_DEFINE_LATE),
-    JS_SELF_HOSTED_FN(js_lookupGetter_str, "ObjectLookupGetter",    1,JSPROP_DEFINE_LATE),
-    JS_SELF_HOSTED_FN(js_lookupSetter_str, "ObjectLookupSetter",    1,JSPROP_DEFINE_LATE),
+    JS_SELF_HOSTED_FN(js_defineGetter_str, "ObjectDefineGetter",    2,0),
+    JS_SELF_HOSTED_FN(js_defineSetter_str, "ObjectDefineSetter",    2,0),
+    JS_SELF_HOSTED_FN(js_lookupGetter_str, "ObjectLookupGetter",    1,0),
+    JS_SELF_HOSTED_FN(js_lookupSetter_str, "ObjectLookupSetter",    1,0),
 #endif
     JS_FS_END
 };
 
 static const JSPropertySpec object_properties[] = {
 #if JS_HAS_OBJ_PROTO_PROP
     JS_PSGS("__proto__", ProtoGetter, ProtoSetter, 0),
 #endif
     JS_PS_END
 };
 
 static const JSFunctionSpec object_static_methods[] = {
-    JS_SELF_HOSTED_FN("assign",        "ObjectStaticAssign",        2, JSPROP_DEFINE_LATE),
-    JS_SELF_HOSTED_FN("getPrototypeOf", "ObjectGetPrototypeOf",     1, JSPROP_DEFINE_LATE),
+    JS_SELF_HOSTED_FN("assign",        "ObjectStaticAssign",        2, 0),
+    JS_SELF_HOSTED_FN("getPrototypeOf", "ObjectGetPrototypeOf",     1, 0),
     JS_FN("setPrototypeOf",            obj_setPrototypeOf,          2, 0),
     JS_FN("getOwnPropertyDescriptor",  obj_getOwnPropertyDescriptor,2, 0),
     JS_FN("keys",                      obj_keys,                    1, 0),
-#ifndef RELEASE_BUILD
-    JS_SELF_HOSTED_FN("values",        "ObjectValues",              1, JSPROP_DEFINE_LATE),
-    JS_SELF_HOSTED_FN("entries",       "ObjectEntries",             1, JSPROP_DEFINE_LATE),
-#endif
+    JS_FN("values",                    obj_values,                  1, 0),
+    JS_FN("entries",                   obj_entries,                 1, 0),
     JS_FN("is",                        obj_is,                      2, 0),
     JS_FN("defineProperty",            obj_defineProperty,          3, 0),
     JS_FN("defineProperties",          obj_defineProperties,        2, 0),
     JS_INLINABLE_FN("create",          obj_create,                  2, 0, ObjectCreate),
     JS_FN("getOwnPropertyNames",       obj_getOwnPropertyNames,     1, 0),
     JS_FN("getOwnPropertySymbols",     obj_getOwnPropertySymbols,   1, 0),
-    JS_SELF_HOSTED_FN("isExtensible",  "ObjectIsExtensible",        1, JSPROP_DEFINE_LATE),
+    JS_SELF_HOSTED_FN("isExtensible",  "ObjectIsExtensible",        1, 0),
     JS_FN("preventExtensions",         obj_preventExtensions,       1, 0),
     JS_FN("freeze",                    obj_freeze,                  1, 0),
     JS_FN("isFrozen",                  obj_isFrozen,                1, 0),
     JS_FN("seal",                      obj_seal,                    1, 0),
     JS_FN("isSealed",                  obj_isSealed,                1, 0),
     JS_FS_END
 };
 
@@ -1084,32 +1202,16 @@ FinishObjectClassInit(JSContext* cx, JS:
         return false;
     global->setOriginalEval(evalobj);
 
     Rooted<NativeObject*> holder(cx, GlobalObject::getIntrinsicsHolder(cx, global));
     if (!holder)
         return false;
 
     /*
-     * Define self-hosted functions on Object and Function after setting the
-     * intrinsics holder (which is needed to define self-hosted functions).
-     */
-    if (!cx->runtime()->isSelfHostingGlobal(global)) {
-        if (!JS_DefineFunctions(cx, ctor, object_static_methods, OnlyDefineLateProperties))
-            return false;
-        if (!JS_DefineFunctions(cx, proto, object_methods, OnlyDefineLateProperties))
-            return false;
-        RootedObject funProto(cx, global->getOrCreateFunctionPrototype(cx));
-        if (!funProto)
-            return false;
-        if (!JS_DefineFunctions(cx, funProto, function_methods, OnlyDefineLateProperties))
-            return false;
-    }
-
-    /*
      * The global object should have |Object.prototype| as its [[Prototype]].
      * Eventually we'd like to have standard classes be there from the start,
      * and thus we would know we were always setting what had previously been a
      * null [[Prototype]], but right now some code assumes it can set the
      * [[Prototype]] before standard classes have been initialized.  For now,
      * only set the [[Prototype]] if it hasn't already been set.
      */
     Rooted<TaggedProto> tagged(cx, TaggedProto(proto));
--- a/js/src/builtin/Object.js
+++ b/js/src/builtin/Object.js
@@ -134,54 +134,8 @@ function ObjectLookupGetter(name) {
         if (desc) {
             if (callFunction(std_Object_hasOwnProperty, desc, "get"))
                 return desc.get;
             return undefined;
         }
         object = std_Reflect_getPrototypeOf(object);
     } while (object !== null);
 }
-
-// Draft proposal http://tc39.github.io/proposal-object-values-entries/#Object.values
-function ObjectValues(O) {
-    // Steps 1-2.
-    var object = ToObject(O);
-
-    // Steps 3-4.
-    // EnumerableOwnProperties is inlined here.
-    var keys = OwnPropertyKeys(object, JSITER_OWNONLY | JSITER_HIDDEN);
-    var values = [];
-    var valuesCount = 0;
-    for (var i = 0; i < keys.length; i++) {
-        var key = keys[i];
-        if (!callFunction(std_Object_propertyIsEnumerable, object, key))
-            continue;
-
-        var value = object[key];
-        _DefineDataProperty(values, valuesCount++, value);
-    }
-
-    // Step 5.
-    return values;
-}
-
-// Draft proposal http://tc39.github.io/proposal-object-values-entries/#Object.entries
-function ObjectEntries(O) {
-    // Steps 1-2.
-    var object = ToObject(O);
-
-    // Steps 3-4.
-    // EnumerableOwnProperties is inlined here.
-    var keys = OwnPropertyKeys(object, JSITER_OWNONLY | JSITER_HIDDEN);
-    var entries = [];
-    var entriesCount = 0;
-    for (var i = 0; i < keys.length; i++) {
-        var key = keys[i];
-        if (!callFunction(std_Object_propertyIsEnumerable, object, key))
-            continue;
-
-        var value = object[key];
-        _DefineDataProperty(entries, entriesCount++, [key, value]);
-    }
-
-    // Step 5.
-    return entries;
-}
--- a/js/src/builtin/TestingFunctions.cpp
+++ b/js/src/builtin/TestingFunctions.cpp
@@ -520,29 +520,29 @@ WasmTextToBinary(JSContext* cx, unsigned
         ReportUsageError(cx, callee, "First argument must be a String");
         return false;
     }
 
     AutoStableStringChars twoByteChars(cx);
     if (!twoByteChars.initTwoByte(cx, args[0].toString()))
         return false;
 
+    wasm::Bytes bytes;
     UniqueChars error;
-    wasm::UniqueBytecode bytes = wasm::TextToBinary(twoByteChars.twoByteChars(), &error);
-    if (!bytes) {
+    if (!wasm::TextToBinary(twoByteChars.twoByteChars(), &bytes, &error)) {
         JS_ReportErrorNumber(cx, GetErrorMessage, nullptr, JSMSG_WASM_TEXT_FAIL,
                              error.get() ? error.get() : "out of memory");
         return false;
     }
 
-    RootedObject obj(cx, JS_NewUint8Array(cx, bytes->length()));
+    RootedObject obj(cx, JS_NewUint8Array(cx, bytes.length()));
     if (!obj)
         return false;
 
-    memcpy(obj->as<TypedArrayObject>().viewDataUnshared(), bytes->begin(), bytes->length());
+    memcpy(obj->as<TypedArrayObject>().viewDataUnshared(), bytes.begin(), bytes.length());
 
     args.rval().setObject(*obj);
     return true;
 }
 
 static bool
 IsLazyFunction(JSContext* cx, unsigned argc, Value* vp)
 {
--- a/js/src/gc/StoreBuffer.h
+++ b/js/src/gc/StoreBuffer.h
@@ -309,18 +309,18 @@ class StoreBuffer
                    (start <= otherEnd && otherEnd <= end);
         }
 
         // Destructively make this SlotsEdge range the union of the other
         // SlotsEdge range and this one. A precondition is that the ranges must
         // overlap.
         void merge(const SlotsEdge& other) {
             MOZ_ASSERT(overlaps(other));
-            auto end = std::max(start_ + count_, other.start_ + other.count_);
-            start_ = std::min(start_, other.start_);
+            auto end = Max(start_ + count_, other.start_ + other.count_);
+            start_ = Min(start_, other.start_);
             count_ = end - start_;
         }
 
         bool maybeInRememberedSet(const Nursery& n) const {
             return !IsInsideNursery(reinterpret_cast<Cell*>(object()));
         }
 
         void trace(TenuringTracer& mover) const;
--- a/js/src/jit-test/tests/wasm/basic-const.js
+++ b/js/src/jit-test/tests/wasm/basic-const.js
@@ -36,19 +36,21 @@ testConst('i32', '0xffffffff', -1);
 //testConst('i64', '18446744073709551615', -1); // TODO: NYI
 //testConst('i64', '-9223372036854775808', -9223372036854775808); // TODO: NYI
 //testConst('i64', '0x7fffffffffffffff', 9223372036854775807); // TODO: NYI
 //testConst('i64', '0x8000000000000000', -9223372036854775808); // TODO: NYI
 //testConst('i64', '-0x8000000000000000', -9223372036854775808); // TODO: NYI
 //testConst('i64', '0xffffffffffffffff', -1); // TODO: NYI
 
 testConst('f32', '0.0', 0.0);
+testConst('f32', '-0', -0.0);
 testConst('f32', '-0.0', -0.0);
 testConst('f32', '0x0.0', 0.0);
 testConst('f32', '-0x0.0', -0.0);
+testConst('f32', '-0x0', -0.0);
 testConst('f32', '0x0.0p0', 0.0);
 testConst('f32', '-0x0.0p0', -0.0);
 testConst('f32', 'infinity', Infinity);
 testConst('f32', '-infinity', -Infinity);
 testConst('f32', '+infinity', Infinity);
 testConst('f32', 'nan', NaN);
 //testConst('f32', '-nan', NaN); // TODO: NYI
 testConst('f32', '+nan', NaN);
@@ -121,18 +123,20 @@ testConst('f32', '0x0434.234p0', 1076.13
 testConst('f32', '-0x3434.234p0', -13364.1376953125);
 testConst('f32', '0x4.22342p0', 4.133607864379883);
 testConst('f32', '0x30000p-20', 1.875000e-01);
 testConst('f32', '0x0.533fcccp-125', 7.645233588931088e-39);
 testConst('f32', '0', 0);
 
 testConst('f64', '0.0', 0.0);
 testConst('f64', '-0.0', -0.0);
+testConst('f64', '-0', -0.0);
 testConst('f64', '0x0.0', 0.0);
 testConst('f64', '-0x0.0', -0.0);
+testConst('f64', '-0x0', -0.0);
 testConst('f64', '0x0.0p0', 0.0);
 testConst('f64', '-0x0.0p0', -0.0);
 testConst('f64', 'infinity', Infinity);
 testConst('f64', '-infinity', -Infinity);
 testConst('f64', '+infinity', Infinity);
 testConst('f64', 'nan', NaN);
 //testConst('f64', '-nan', NaN); // TODO: NYI
 testConst('f64', '+nan', NaN);
--- a/js/src/jit-test/tests/wasm/basic-memory.js
+++ b/js/src/jit-test/tests/wasm/basic-memory.js
@@ -1,13 +1,10 @@
 load(libdir + "wasm.js");
 
-if (!wasmIsSupported())
-    quit();
-
 function testLoad(type, ext, base, offset, align, expect) {
   assertEq(wasmEvalText(
     '(module' +
     '  (memory 1' +
     '    (segment 0 "\\00\\01\\02\\03\\04\\05\\06\\07\\08\\09\\0a\\0b\\0c\\0d\\0e\\0f")' +
     '    (segment 16 "\\f0\\f1\\f2\\f3\\f4\\f5\\f6\\f7\\f8\\f9\\fa\\fb\\fc\\fd\\fe\\ff")' +
     '  )' +
     '  (func (param i32) (result ' + type + ')' +
--- a/js/src/jit-test/tests/wasm/binary.js
+++ b/js/src/jit-test/tests/wasm/binary.js
@@ -74,26 +74,30 @@ assertErrorMessage(() => wasmEval(toU8(m
 assertErrorMessage(() => wasmEval(toU8(moduleHeaderThen(0, 1))), TypeError, sectionError);
 assertErrorMessage(() => wasmEval(toU8(moduleHeaderThen(0, 0))), TypeError, unknownSectionError);
 
 function cstring(name) {
     return (name + '\0').split('').map(c => c.charCodeAt(0));
 }
 
 function string(name) {
-    return name.split('').map(c => c.charCodeAt(0));
+    var nameBytes = name.split('').map(c => {
+        var code = c.charCodeAt(0);
+        assertEq(code < 128, true); // TODO
+        return code
+    });
+    return varU32(nameBytes.length).concat(nameBytes);
 }
 
 function moduleWithSections(sectionArray) {
     var bytes = moduleHeaderThen();
     for (let section of sectionArray) {
-        var nameLength = varU32(section.name.length);
-        bytes.push(...varU32(nameLength.length + section.name.length + section.body.length));
-        bytes.push(...nameLength);
-        bytes.push(...string(section.name));
+        var sectionName = string(section.name);
+        bytes.push(...varU32(sectionName.length + section.body.length));
+        bytes.push(...sectionName);
         bytes.push(...section.body);
     }
     return toU8(bytes);
 }
 
 function sigSection(sigs) {
     var body = [];
     body.push(...varU32(sigs.length));
@@ -128,18 +132,18 @@ function bodySection(bodies) {
     return { name: functionBodiesId, body };
 }
 
 function importSection(imports) {
     var body = [];
     body.push(...varU32(imports.length));
     for (let imp of imports) {
         body.push(...varU32(imp.sigIndex));
-        body.push(...cstring(imp.module));
-        body.push(...cstring(imp.func));
+        body.push(...string(imp.module));
+        body.push(...string(imp.func));
     }
     return { name: importId, body };
 }
 
 function tableSection(elems) {
     var body = [];
     body.push(...varU32(elems.length));
     for (let i of elems)
@@ -158,18 +162,18 @@ assertErrorMessage(() => wasmEval(module
 assertThrowsInstanceOf(() => wasmEval(moduleWithSections([{name: sigId, body: [1]}])), TypeError);
 assertThrowsInstanceOf(() => wasmEval(moduleWithSections([{name: sigId, body: [1, 1, 0]}])), TypeError);
 
 wasmEval(moduleWithSections([sigSection([])]));
 wasmEval(moduleWithSections([sigSection([v2vSig])]));
 wasmEval(moduleWithSections([sigSection([i2vSig])]));
 wasmEval(moduleWithSections([sigSection([v2vSig, i2vSig])]));
 
-assertErrorMessage(() => wasmEval(moduleWithSections([sigSection([{args:[], ret:100}])])), TypeError, /bad expression type/);
-assertErrorMessage(() => wasmEval(moduleWithSections([sigSection([{args:[100], ret:VoidCode}])])), TypeError, /bad value type/);
+assertErrorMessage(() => wasmEval(moduleWithSections([sigSection([{args:[], ret:100}])])), TypeError, /expression type/);
+assertErrorMessage(() => wasmEval(moduleWithSections([sigSection([{args:[100], ret:VoidCode}])])), TypeError, /value type/);
 
 assertThrowsInstanceOf(() => wasmEval(moduleWithSections([sigSection([]), declSection([0])])), TypeError, /signature index out of range/);
 assertThrowsInstanceOf(() => wasmEval(moduleWithSections([sigSection([v2vSig]), declSection([1])])), TypeError, /signature index out of range/);
 assertErrorMessage(() => wasmEval(moduleWithSections([sigSection([v2vSig]), declSection([0])])), TypeError, /expected function bodies/);
 wasmEval(moduleWithSections([sigSection([v2vSig]), declSection([0]), bodySection([v2vBody])]));
 
 assertThrowsInstanceOf(() => wasmEval(moduleWithSections([sigSection([v2vSig]), {name: importId, body:[]}])), TypeError);
 assertErrorMessage(() => wasmEval(moduleWithSections([importSection([{sigIndex:0, module:"a", func:"b"}])])), TypeError, /signature index out of range/);
new file mode 100644
--- /dev/null
+++ b/js/src/jit-test/tests/wasm/memory-aliasing.js
@@ -0,0 +1,26 @@
+load(libdir + "wasm.js");
+
+var i = wasmEvalText(
+`(module
+   (memory 1 (segment 0 "\\01\\02\\03\\04\\05\\06\\07\\08"))
+   (func $off1 (param $base i32) (result i32)
+     (i32.add
+       (i32.load8_u (get_local $base))
+       (i32.load8_u offset=1 (get_local $base)))
+   )
+   (export "off1" $off1)
+   (func $off2 (param $base i32) (result i32)
+     (i32.add
+       (i32.load8_u offset=1 (get_local $base))
+       (i32.load8_u offset=2 (get_local $base)))
+   )
+   (export "off2" $off2)
+)`);
+assertEq(i.off1(0), 3);
+assertEq(i.off1(1), 5);
+assertEq(i.off1(2), 7);
+assertEq(i.off1(3), 9);
+assertEq(i.off2(0), 5);
+assertEq(i.off2(1), 7);
+assertEq(i.off2(2), 9);
+assertEq(i.off2(3), 11);
--- a/js/src/jit/MIR.cpp
+++ b/js/src/jit/MIR.cpp
@@ -4550,28 +4550,31 @@ MAsmJSLoadHeap::mightAlias(const MDefini
 {
     if (def->isAsmJSStoreHeap()) {
         const MAsmJSStoreHeap* store = def->toAsmJSStoreHeap();
         if (store->accessType() != accessType())
             return true;
         if (!base()->isConstant() || !store->base()->isConstant())
             return true;
         const MConstant* otherBase = store->base()->toConstant();
-        return base()->toConstant()->equals(otherBase);
+        return base()->toConstant()->equals(otherBase) &&
+               offset() == store->offset();
     }
     return true;
 }
 
 bool
 MAsmJSLoadHeap::congruentTo(const MDefinition* ins) const
 {
     if (!ins->isAsmJSLoadHeap())
         return false;
     const MAsmJSLoadHeap* load = ins->toAsmJSLoadHeap();
-    return load->accessType() == accessType() && congruentIfOperandsEqual(load);
+    return load->accessType() == accessType() &&
+           load->offset() == offset() &&
+           congruentIfOperandsEqual(load);
 }
 
 bool
 MAsmJSLoadGlobalVar::mightAlias(const MDefinition* def) const
 {
     if (def->isAsmJSStoreGlobalVar()) {
         const MAsmJSStoreGlobalVar* store = def->toAsmJSStoreGlobalVar();
         return store->globalDataOffset() == globalDataOffset_;
--- a/js/src/jsapi-tests/testWasmLEB128.cpp
+++ b/js/src/jsapi-tests/testWasmLEB128.cpp
@@ -38,73 +38,73 @@ static bool WriteValidBytes(js::wasm::En
     return true;
 }
 
 BEGIN_TEST(testWasmLEB128_encoding)
 {
     using namespace js;
     using namespace wasm;
 
-    Bytecode bc;
-    Encoder encoder(bc);
+    Bytes bytes;
+    Encoder encoder(bytes);
 
     bool passed;
     if (!WriteValidBytes(encoder, &passed))
         return false;
     CHECK(passed);
 
     size_t i = 0;
-    CHECK(bc[i++] == 0x0);
-    CHECK(bc[i++] == 0x1);
-    CHECK(bc[i++] == 0x42);
+    CHECK(bytes[i++] == 0x0);
+    CHECK(bytes[i++] == 0x1);
+    CHECK(bytes[i++] == 0x42);
 
-    CHECK(bc[i++] == 0x80);
-    CHECK(bc[i++] == 0x01);
+    CHECK(bytes[i++] == 0x80);
+    CHECK(bytes[i++] == 0x01);
 
-    CHECK(bc[i++] == 0x80);
-    CHECK(bc[i++] == 0x03);
+    CHECK(bytes[i++] == 0x80);
+    CHECK(bytes[i++] == 0x03);
 
-    if (i + 1 < bc.length())
-        CHECK(bc[i++] == 0x00);
+    if (i + 1 < bytes.length())
+        CHECK(bytes[i++] == 0x00);
     return true;
 }
 END_TEST(testWasmLEB128_encoding)
 
 BEGIN_TEST(testWasmLEB128_valid_decoding)
 {
     using namespace js;
     using namespace wasm;
 
-    Bytecode bc;
-    if (!bc.append(0x0) || !bc.append(0x1) || !bc.append(0x42))
+    Bytes bytes;
+    if (!bytes.append(0x0) || !bytes.append(0x1) || !bytes.append(0x42))
         return false;
 
-    if (!bc.append(0x80) || !bc.append(0x01))
+    if (!bytes.append(0x80) || !bytes.append(0x01))
         return false;
 
-    if (!bc.append(0x80) || !bc.append(0x03))
+    if (!bytes.append(0x80) || !bytes.append(0x03))
         return false;
 
     {
         // Fallible decoding
-        Decoder decoder(bc);
+        Decoder decoder(bytes);
         uint32_t value;
 
         CHECK(decoder.readVarU32(&value) && value == 0x0);
         CHECK(decoder.readVarU32(&value) && value == 0x1);
         CHECK(decoder.readVarU32(&value) && value == 0x42);
         CHECK(decoder.readVarU32(&value) && value == 0x80);
         CHECK(decoder.readVarU32(&value) && value == 0x180);
 
         CHECK(decoder.done());
     }
 
     {
         // Infallible decoding
-        Decoder decoder(bc);
+        Decoder decoder(bytes);
         uint32_t value;
 
         value = decoder.uncheckedReadVarU32();
         CHECK(value == 0x0);
         value = decoder.uncheckedReadVarU32();
         CHECK(value == 0x1);
         value = decoder.uncheckedReadVarU32();
         CHECK(value == 0x42);
@@ -119,48 +119,48 @@ BEGIN_TEST(testWasmLEB128_valid_decoding
 }
 END_TEST(testWasmLEB128_valid_decoding)
 
 BEGIN_TEST(testWasmLEB128_invalid_decoding)
 {
     using namespace js;
     using namespace wasm;
 
-    Bytecode bc;
+    Bytes bytes;
     // Fill bits as per 28 encoded bits
-    if (!bc.append(0x80) || !bc.append(0x80) || !bc.append(0x80) || !bc.append(0x80))
+    if (!bytes.append(0x80) || !bytes.append(0x80) || !bytes.append(0x80) || !bytes.append(0x80))
         return false;
 
     // Test last valid values
-    if (!bc.append(0x00))
+    if (!bytes.append(0x00))
         return false;
 
     for (uint8_t i = 0; i < 0x0F; i++) {
-        bc[4] = i;
+        bytes[4] = i;
 
         {
-            Decoder decoder(bc);
+            Decoder decoder(bytes);
             uint32_t value;
             CHECK(decoder.readVarU32(&value));
             CHECK(value == uint32_t(i << 28));
             CHECK(decoder.done());
         }
 
         {
-            Decoder decoder(bc);
+            Decoder decoder(bytes);
             uint32_t value = decoder.uncheckedReadVarU32();
             CHECK(value == uint32_t(i << 28));
             CHECK(decoder.done());
         }
     }
 
     // Test all invalid values of the same size
     for (uint8_t i = 0x10; i < 0xF0; i++) {
-        bc[4] = i;
+        bytes[4] = i;
 
-        Decoder decoder(bc);
+        Decoder decoder(bytes);
         uint32_t value;
         CHECK(!decoder.readVarU32(&value));
     }
 
     return true;
 }
 END_TEST(testWasmLEB128_invalid_decoding)
--- a/js/src/jsapi.cpp
+++ b/js/src/jsapi.cpp
@@ -3664,25 +3664,24 @@ JS_IsNativeFunction(JSObject* funobj, JS
 
 extern JS_PUBLIC_API(bool)
 JS_IsConstructor(JSFunction* fun)
 {
     return fun->isConstructor();
 }
 
 JS_PUBLIC_API(bool)
-JS_DefineFunctions(JSContext* cx, HandleObject obj, const JSFunctionSpec* fs,
-                   PropertyDefinitionBehavior behavior)
+JS_DefineFunctions(JSContext* cx, HandleObject obj, const JSFunctionSpec* fs)
 {
     MOZ_ASSERT(!cx->runtime()->isAtomsCompartment(cx->compartment()));
     AssertHeapIsIdle(cx);
     CHECK_REQUEST(cx);
     assertSameCompartment(cx, obj);
 
-    return DefineFunctions(cx, obj, fs, NotIntrinsic, behavior);
+    return DefineFunctions(cx, obj, fs, NotIntrinsic);
 }
 
 JS_PUBLIC_API(JSFunction*)
 JS_DefineFunction(JSContext* cx, HandleObject obj, const char* name, JSNative call,
                   unsigned nargs, unsigned attrs)
 {
     MOZ_ASSERT(!cx->runtime()->isAtomsCompartment(cx->compartment()));
     AssertHeapIsIdle(cx);
--- a/js/src/jsapi.h
+++ b/js/src/jsapi.h
@@ -829,20 +829,17 @@ class MOZ_STACK_CLASS SourceBufferHolder
 #define JSPROP_GETTER           0x10    /* property holds getter function */
 #define JSPROP_SETTER           0x20    /* property holds setter function */
 #define JSPROP_SHARED           0x40    /* don't allocate a value slot for this
                                            property; don't copy the property on
                                            set of the same-named property in an
                                            object that delegates to a prototype
                                            containing this property */
 #define JSPROP_INTERNAL_USE_BIT 0x80    /* internal JS engine use only */
-#define JSPROP_DEFINE_LATE     0x100    /* Don't define property when initially creating
-                                           the constructor. Some objects like Function/Object
-                                           have self-hosted functions that can only be defined
-                                           after the initialization is already finished. */
+//                             0x100    /* Unused */
 #define JSFUN_STUB_GSOPS       0x200    /* use JS_PropertyStub getter/setter
                                            instead of defaulting to class gsops
                                            for property holding function */
 
 #define JSFUN_CONSTRUCTOR      0x400    /* native that can be called as a ctor */
 
 /*
  * Specify a generic native prototype methods, i.e., methods of a class
@@ -3588,30 +3585,18 @@ JS_ObjectIsFunction(JSContext* cx, JSObj
 
 extern JS_PUBLIC_API(bool)
 JS_IsNativeFunction(JSObject* funobj, JSNative call);
 
 /** Return whether the given function is a valid constructor. */
 extern JS_PUBLIC_API(bool)
 JS_IsConstructor(JSFunction* fun);
 
-/**
- * This enum is used to select if properties with JSPROP_DEFINE_LATE flag
- * should be defined on the object.
- * Normal JSAPI consumers probably always want DefineAllProperties here.
- */
-enum PropertyDefinitionBehavior {
-    DefineAllProperties,
-    OnlyDefineLateProperties,
-    DontDefineLateProperties
-};
-
-extern JS_PUBLIC_API(bool)
-JS_DefineFunctions(JSContext* cx, JS::Handle<JSObject*> obj, const JSFunctionSpec* fs,
-                   PropertyDefinitionBehavior behavior = DefineAllProperties);
+extern JS_PUBLIC_API(bool)
+JS_DefineFunctions(JSContext* cx, JS::Handle<JSObject*> obj, const JSFunctionSpec* fs);
 
 extern JS_PUBLIC_API(JSFunction*)
 JS_DefineFunction(JSContext* cx, JS::Handle<JSObject*> obj, const char* name, JSNative call,
                   unsigned nargs, unsigned attrs);
 
 extern JS_PUBLIC_API(JSFunction*)
 JS_DefineUCFunction(JSContext* cx, JS::Handle<JSObject*> obj,
                     const char16_t* name, size_t namelen, JSNative call,
--- a/js/src/jsarray.cpp
+++ b/js/src/jsarray.cpp
@@ -3674,16 +3674,30 @@ js::NewCopiedArrayForCallingAllocationSi
                                            HandleObject proto /* = nullptr */)
 {
     RootedObjectGroup group(cx, ObjectGroup::callingAllocationSiteGroup(cx, JSProto_Array, proto));
     if (!group)
         return nullptr;
     return NewCopiedArrayTryUseGroup(cx, group, vp, length);
 }
 
+bool
+js::NewValuePair(JSContext* cx, const Value& val1, const Value& val2, MutableHandleValue rval)
+{
+    JS::AutoValueArray<2> vec(cx);
+    vec[0].set(val1);
+    vec[1].set(val2);
+
+    JSObject* aobj = js::NewDenseCopiedArray(cx, 2, vec.begin());
+    if (!aobj)
+        return false;
+    rval.setObject(*aobj);
+    return true;
+}
+
 #ifdef DEBUG
 bool
 js::ArrayInfo(JSContext* cx, unsigned argc, Value* vp)
 {
     CallArgs args = CallArgsFromVp(argc, vp);
     JSObject* obj;
 
     for (unsigned i = 0; i < args.length(); i++) {
--- a/js/src/jsarray.h
+++ b/js/src/jsarray.h
@@ -114,16 +114,19 @@ NewCopiedArrayTryUseGroup(ExclusiveConte
                           const Value* vp, size_t length,
                           NewObjectKind newKind = GenericObject,
                           ShouldUpdateTypes updateTypes = ShouldUpdateTypes::Update);
 
 extern JSObject*
 NewCopiedArrayForCallingAllocationSite(JSContext* cx, const Value* vp, size_t length,
                                        HandleObject proto = nullptr);
 
+extern bool
+NewValuePair(JSContext* cx, const Value& val1, const Value& val2, MutableHandleValue rval);
+
 /*
  * Determines whether a write to the given element on |obj| should fail because
  * |obj| is an Array with a non-writable length, and writing that element would
  * increase the length of the array.
  */
 extern bool
 WouldDefinePastNonwritableLength(HandleNativeObject obj, uint32_t index);
 
--- a/js/src/jsfun.cpp
+++ b/js/src/jsfun.cpp
@@ -1590,17 +1590,17 @@ OnBadFormal(JSContext* cx)
 const JSFunctionSpec js::function_methods[] = {
 #if JS_HAS_TOSOURCE
     JS_FN(js_toSource_str,   fun_toSource,   0,0),
 #endif
     JS_FN(js_toString_str,   fun_toString,   0,0),
     JS_FN(js_apply_str,      fun_apply,      2,0),
     JS_FN(js_call_str,       fun_call,       1,0),
     JS_FN("isGenerator",     fun_isGenerator,0,0),
-    JS_SELF_HOSTED_FN("bind", "FunctionBind", 2,JSPROP_DEFINE_LATE|JSFUN_HAS_REST),
+    JS_SELF_HOSTED_FN("bind", "FunctionBind", 2,JSFUN_HAS_REST),
     JS_FS_END
 };
 
 static bool
 FunctionConstructor(JSContext* cx, unsigned argc, Value* vp, GeneratorKind generatorKind)
 {
     CallArgs args = CallArgsFromVp(argc, vp);
 
--- a/js/src/jsiter.cpp
+++ b/js/src/jsiter.cpp
@@ -79,25 +79,17 @@ struct IdHashPolicy {
     }
 };
 
 typedef HashSet<jsid, IdHashPolicy> IdSet;
 
 static inline bool
 NewKeyValuePair(JSContext* cx, jsid id, const Value& val, MutableHandleValue rval)
 {
-    JS::AutoValueArray<2> vec(cx);
-    vec[0].set(IdToValue(id));
-    vec[1].set(val);
-
-    JSObject* aobj = NewDenseCopiedArray(cx, 2, vec.begin());
-    if (!aobj)
-        return false;
-    rval.setObject(*aobj);
-    return true;
+    return NewValuePair(cx, IdToValue(id), val, rval);
 }
 
 static inline bool
 Enumerate(JSContext* cx, HandleObject pobj, jsid id,
           bool enumerable, unsigned flags, Maybe<IdSet>& ht, AutoIdVector* props)
 {
     // Allow duplicate properties from Proxy's [[OwnPropertyKeys]].
     bool proxyOwnProperty = pobj->is<ProxyObject>() && (flags & JSITER_OWNONLY);
--- a/js/src/jsobj.cpp
+++ b/js/src/jsobj.cpp
@@ -2902,34 +2902,20 @@ DefineFunctionFromSpec(JSContext* cx, Ha
         fun->setIsIntrinsic();
 
     RootedValue funVal(cx, ObjectValue(*fun));
     return DefineProperty(cx, obj, id, funVal, gop, sop, flags & ~JSFUN_FLAGS_MASK);
 }
 
 bool
 js::DefineFunctions(JSContext* cx, HandleObject obj, const JSFunctionSpec* fs,
-                    DefineAsIntrinsic intrinsic, PropertyDefinitionBehavior behavior)
+                    DefineAsIntrinsic intrinsic)
 {
     for (; fs->name; fs++) {
-        unsigned flags = fs->flags;
-        switch (behavior) {
-          case DefineAllProperties:
-            break;
-          case OnlyDefineLateProperties:
-            if (!(flags & JSPROP_DEFINE_LATE))
-                continue;
-            break;
-          default:
-            MOZ_ASSERT(behavior == DontDefineLateProperties);
-            if (flags & JSPROP_DEFINE_LATE)
-                continue;
-        }
-
-        if (!DefineFunctionFromSpec(cx, obj, fs, flags & ~JSPROP_DEFINE_LATE, intrinsic))
+        if (!DefineFunctionFromSpec(cx, obj, fs, fs->flags, intrinsic))
             return false;
     }
     return true;
 }
 
 
 /*** ToPrimitive *************************************************************/
 
--- a/js/src/jsobj.h
+++ b/js/src/jsobj.h
@@ -987,18 +987,17 @@ HasOwnProperty(JSContext* cx, HandleObje
  */
 enum DefineAsIntrinsic {
     NotIntrinsic,
     AsIntrinsic
 };
 
 extern bool
 DefineFunctions(JSContext* cx, HandleObject obj, const JSFunctionSpec* fs,
-                DefineAsIntrinsic intrinsic,
-                PropertyDefinitionBehavior behavior = DefineAllProperties);
+                DefineAsIntrinsic intrinsic);
 
 /*
  * Set a watchpoint: a synchronous callback when the given property of the
  * given object is set.
  *
  * Watchpoints are nonstandard and do not fit in well with the way ES6
  * specifies [[Set]]. They are also insufficient for implementing
  * Object.observe.
--- a/js/src/moz.build
+++ b/js/src/moz.build
@@ -142,16 +142,17 @@ EXPORTS.js += [
     '../public/Value.h',
     '../public/Vector.h',
     '../public/WeakMapPtr.h',
 ]
 
 UNIFIED_SOURCES += [
     'asmjs/AsmJS.cpp',
     'asmjs/Wasm.cpp',
+    'asmjs/WasmBinary.cpp',
     'asmjs/WasmFrameIterator.cpp',
     'asmjs/WasmGenerator.cpp',
     'asmjs/WasmIonCompile.cpp',
     'asmjs/WasmModule.cpp',
     'asmjs/WasmSignalHandlers.cpp',
     'asmjs/WasmStubs.cpp',
     'asmjs/WasmText.cpp',
     'asmjs/WasmTypes.cpp',
--- a/js/src/vm/GlobalObject.cpp
+++ b/js/src/vm/GlobalObject.cpp
@@ -215,28 +215,30 @@ GlobalObject::resolveConstructor(JSConte
         if (!global->addDataProperty(cx, id, constructorPropertySlot(key), 0))
             return false;
     }
 
     global->setConstructor(key, ObjectValue(*ctor));
     global->setConstructorPropertySlot(key, ObjectValue(*ctor));
 
     // Define any specified functions and properties, unless we're a dependent
-    // standard class (in which case they live on the prototype).
-    if (!StandardClassIsDependent(key)) {
+    // standard class (in which case they live on the prototype), or we're
+    // operating on the self-hosting global, in which case we don't want any
+    // functions and properties on the builtins and their prototypes.
+    if (!StandardClassIsDependent(key) && !cx->runtime()->isSelfHostingGlobal(global)) {
         if (const JSFunctionSpec* funs = clasp->spec.prototypeFunctions()) {
-            if (!JS_DefineFunctions(cx, proto, funs, DontDefineLateProperties))
+            if (!JS_DefineFunctions(cx, proto, funs))
                 return false;
         }
         if (const JSPropertySpec* props = clasp->spec.prototypeProperties()) {
             if (!JS_DefineProperties(cx, proto, props))
                 return false;
         }
         if (const JSFunctionSpec* funs = clasp->spec.constructorFunctions()) {
-            if (!JS_DefineFunctions(cx, ctor, funs, DontDefineLateProperties))
+            if (!JS_DefineFunctions(cx, ctor, funs))
                 return false;
         }
         if (const JSPropertySpec* props = clasp->spec.constructorProperties()) {
             if (!JS_DefineProperties(cx, ctor, props))
                 return false;
         }
     }
 
--- a/js/src/vm/SelfHosting.cpp
+++ b/js/src/vm/SelfHosting.cpp
@@ -132,16 +132,69 @@ intrinsic_IsCallable(JSContext* cx, unsi
 static bool
 intrinsic_IsConstructor(JSContext* cx, unsigned argc, Value* vp)
 {
     CallArgs args = CallArgsFromVp(argc, vp);
     args.rval().setBoolean(IsConstructor(args[0]));
     return true;
 }
 
+/**
+ * Intrinsic for calling a wrapped self-hosted function without invoking the
+ * wrapper's security checks.
+ *
+ * Takes a wrapped function as the first and the receiver object as the
+ * second argument. Any additional arguments are passed on to the unwrapped
+ * function.
+ *
+ * Xray wrappers prevent lower-privileged code from passing objects to wrapped
+ * functions from higher-privileged realms. In some cases, this check is too
+ * strict, so this intrinsic allows getting around it.
+ *
+ * Note that it's not possible to replace all usages with dedicated intrinsics
+ * as the function in question might be an inner function that closes over
+ * state relevant to its execution.
+ *
+ * Right now, this is used for the Promise implementation to enable creating
+ * resolution functions for xrayed Promises in the privileged realm and then
+ * creating the Promise instance in the non-privileged one. The callbacks have
+ * to be called by non-privileged code in various places, in many cases
+ * passing objects as arguments.
+ */
+static bool
+intrinsic_UnsafeCallWrappedFunction(JSContext* cx, unsigned argc, Value* vp)
+{
+    CallArgs args = CallArgsFromVp(argc, vp);
+    MOZ_ASSERT(args.length() >= 2);
+    MOZ_ASSERT(IsCallable(args[0]));
+    MOZ_ASSERT(IsWrapper(&args[0].toObject()));
+    MOZ_ASSERT(args[1].isObject() || args[1].isUndefined());
+
+    MOZ_RELEASE_ASSERT(args[0].isObject());
+    RootedObject wrappedFun(cx, &args[0].toObject());
+    RootedObject fun(cx, UncheckedUnwrap(wrappedFun));
+    MOZ_RELEASE_ASSERT(fun->is<JSFunction>());
+    MOZ_RELEASE_ASSERT(fun->as<JSFunction>().isSelfHostedBuiltin());
+
+    InvokeArgs args2(cx);
+    if (!args2.init(args.length() - 2))
+        return false;
+
+    args2.setThis(args[1]);
+
+    for (size_t i = 0; i < args2.length(); i++)
+        args2[i].set(args[i + 2]);
+
+    AutoWaivePolicy waivePolicy(cx, wrappedFun, JSID_VOIDHANDLE, BaseProxyHandler::CALL);
+    if (!CrossCompartmentWrapper::singleton.call(cx, wrappedFun, args2))
+        return false;
+    args.rval().set(args2.rval());
+    return true;
+}
+
 template<typename T>
 static bool
 intrinsic_IsInstanceOfBuiltin(JSContext* cx, unsigned argc, Value* vp)
 {
     CallArgs args = CallArgsFromVp(argc, vp);
     MOZ_ASSERT(args.length() == 1);
     MOZ_ASSERT(args[0].isObject());
 
@@ -1837,16 +1890,18 @@ static const JSFunctionSpec intrinsic_fu
                     IntrinsicUnsafeGetObjectFromReservedSlot),
     JS_INLINABLE_FN("UnsafeGetInt32FromReservedSlot",   intrinsic_UnsafeGetInt32FromReservedSlot,  2,0,
                     IntrinsicUnsafeGetInt32FromReservedSlot),
     JS_INLINABLE_FN("UnsafeGetStringFromReservedSlot",  intrinsic_UnsafeGetStringFromReservedSlot, 2,0,
                     IntrinsicUnsafeGetStringFromReservedSlot),
     JS_INLINABLE_FN("UnsafeGetBooleanFromReservedSlot", intrinsic_UnsafeGetBooleanFromReservedSlot,2,0,
                     IntrinsicUnsafeGetBooleanFromReservedSlot),
 
+    JS_FN("UnsafeCallWrappedFunction", intrinsic_UnsafeCallWrappedFunction,2,0),
+
     JS_FN("IsPackedArray",           intrinsic_IsPackedArray,           1,0),
 
     JS_FN("GetIteratorPrototype",    intrinsic_GetIteratorPrototype,    0,0),
 
     JS_FN("NewArrayIterator",        intrinsic_NewArrayIterator,        0,0),
     JS_FN("CallArrayIteratorMethodIfWrapped",
           CallNonGenericSelfhostedMethod<Is<ArrayIteratorObject>>,      2,0),
 
--- a/layout/base/nsPresShell.cpp
+++ b/layout/base/nsPresShell.cpp
@@ -9300,50 +9300,62 @@ PresShell::Observe(nsISupports* aSubject
           --mChangeNestCount;
         }
       }
     }
     return NS_OK;
   }
 #endif
 
-  if (!nsCRT::strcmp(aTopic, "agent-sheet-added") && mStyleSet) {
-    AddAgentSheet(aSubject);
+  if (!nsCRT::strcmp(aTopic, "agent-sheet-added")) {
+    if (mStyleSet) {
+      AddAgentSheet(aSubject);
+    }
     return NS_OK;
   }
 
-  if (!nsCRT::strcmp(aTopic, "user-sheet-added") && mStyleSet) {
-    AddUserSheet(aSubject);
+  if (!nsCRT::strcmp(aTopic, "user-sheet-added")) {
+    if (mStyleSet) {
+      AddUserSheet(aSubject);
+    }
     return NS_OK;
   }
 
-  if (!nsCRT::strcmp(aTopic, "author-sheet-added") && mStyleSet) {
-    AddAuthorSheet(aSubject);
+  if (!nsCRT::strcmp(aTopic, "author-sheet-added")) {
+    if (mStyleSet) {
+      AddAuthorSheet(aSubject);
+    }
     return NS_OK;
   }
 
-  if (!nsCRT::strcmp(aTopic, "agent-sheet-removed") && mStyleSet) {
-    RemoveSheet(SheetType::Agent, aSubject);
+  if (!nsCRT::strcmp(aTopic, "agent-sheet-removed")) {
+    if (mStyleSet) {
+      RemoveSheet(SheetType::Agent, aSubject);
+    }
     return NS_OK;
   }
 
-  if (!nsCRT::strcmp(aTopic, "user-sheet-removed") && mStyleSet) {
-    RemoveSheet(SheetType::User, aSubject);
+  if (!nsCRT::strcmp(aTopic, "user-sheet-removed")) {
+    if (mStyleSet) {
+      RemoveSheet(SheetType::User, aSubject);
+    }
     return NS_OK;
   }
 
-  if (!nsCRT::strcmp(aTopic, "author-sheet-removed") && mStyleSet) {
-    RemoveSheet(SheetType::Doc, aSubject);
+  if (!nsCRT::strcmp(aTopic, "author-sheet-removed")) {
+    if (mStyleSet) {
+      RemoveSheet(SheetType::Doc, aSubject);
+    }
     return NS_OK;
   }
 
-  if (!nsCRT::strcmp(aTopic, "memory-pressure") &&
-      !AssumeAllImagesVisible() &&
-      mPresContext->IsRootContentDocument()) {
-    DoUpdateImageVisibility(/* aRemoveOnly = */ true);
+  if (!nsCRT::strcmp(aTopic, "memory-pressure")) {
+    if (!AssumeAllImagesVisible() && mPresContext->IsRootContentDocument()) {
+      DoUpdateImageVisibility(/* aRemoveOnly = */ true);
+    }
     return NS_OK;
   }
 
   NS_WARNING("unrecognized topic in PresShell::Observe");
   return NS_ERROR_FAILURE;
 }
 
 bool
old mode 100755
new mode 100644
old mode 100755
new mode 100644
--- a/memory/replace/dmd/DMD.cpp
+++ b/memory/replace/dmd/DMD.cpp
@@ -377,17 +377,16 @@ class Options
     // other blocks. This mode disables sampling.
     Scan
   };
 
   char* mDMDEnvVar;   // a saved copy, for later printing
 
   Mode mMode;
   NumOption<size_t> mSampleBelowSize;
-  NumOption<uint32_t> mMaxFrames;
   bool mShowDumpStats;
 
   void BadArg(const char* aArg);
   static const char* ValueIfMatch(const char* aArg, const char* aOptionName);
   static bool GetLong(const char* aArg, const char* aOptionName,
                       long aMin, long aMax, long* aValue);
   static bool GetBool(const char* aArg, const char* aOptionName, bool* aValue);
 
@@ -399,17 +398,16 @@ public:
   bool IsCumulativeMode() const { return mMode == Cumulative; }
   bool IsScanMode()       const { return mMode == Scan; }
 
   const char* ModeString() const;
 
   const char* DMDEnvVar() const { return mDMDEnvVar; }
 
   size_t SampleBelowSize() const { return mSampleBelowSize.mActual; }
-  size_t MaxFrames()       const { return mMaxFrames.mActual; }
   size_t ShowDumpStats()   const { return mShowDumpStats; }
 };
 
 static Options *gOptions;
 
 //---------------------------------------------------------------------------
 // The global lock
 //---------------------------------------------------------------------------
@@ -690,19 +688,17 @@ typedef CodeAddressService<StringTable, 
 
 class StackTrace
 {
 public:
   static const uint32_t MaxFrames = 24;
 
 private:
   uint32_t mLength;             // The number of PCs.
-  const void* mPcs[MaxFrames];  // The PCs themselves.  If --max-frames is less
-                                // than 24, this array is bigger than
-                                // necessary, but that case is unusual.
+  const void* mPcs[MaxFrames];  // The PCs themselves.
 
 public:
   StackTrace() : mLength(0) {}
 
   uint32_t Length() const { return mLength; }
   const void* Pc(uint32_t i) const
   {
     MOZ_ASSERT(i < mLength);
@@ -774,17 +770,17 @@ StackTrace::Get(Thread* aT)
   // https://bugzilla.mozilla.org/show_bug.cgi?id=374829#c8
   // On Linux, something similar can happen;  see bug 824340.
   // So let's just release it on all platforms.
   StackTrace tmp;
   {
     AutoUnlockState unlock;
     uint32_t skipFrames = 2;
     if (MozStackWalk(StackWalkCallback, skipFrames,
-                      gOptions->MaxFrames(), &tmp, 0, nullptr)) {
+                     MaxFrames, &tmp, 0, nullptr)) {
       // Handle the common case first.  All is ok.  Nothing to do.
     } else {
       tmp.mLength = 0;
     }
   }
 
   StackTraceTable::AddPtr p = gStackTraceTable->lookupForAdd(&tmp);
   if (!p) {
@@ -1425,17 +1421,16 @@ Options::GetBool(const char* aArg, const
 //   values, because jemalloc always rounds up requests sizes.  In contrast, a
 //   prime size will explore all possible values of the alloc counter.
 //
 Options::Options(const char* aDMDEnvVar)
   : mDMDEnvVar(aDMDEnvVar ? InfallibleAllocPolicy::strdup_(aDMDEnvVar)
                           : nullptr)
   , mMode(DarkMatter)
   , mSampleBelowSize(4093, 100 * 100 * 1000)
-  , mMaxFrames(StackTrace::MaxFrames, StackTrace::MaxFrames)
   , mShowDumpStats(false)
 {
   // It's no longer necessary to set the DMD env var to "1" if you want default
   // options (you can leave it undefined) but we still accept "1" for
   // backwards compatibility.
   char* e = mDMDEnvVar;
   if (e && strcmp(e, "1") != 0) {
     bool isEnd = false;
@@ -1468,19 +1463,16 @@ Options::Options(const char* aDMDEnvVar)
         mMode = Options::Cumulative;
       } else if (strcmp(arg, "--mode=scan") == 0) {
         mMode = Options::Scan;
 
       } else if (GetLong(arg, "--sample-below", 1, mSampleBelowSize.mMax,
                  &myLong)) {
         mSampleBelowSize.mActual = myLong;
 
-      } else if (GetLong(arg, "--max-frames", 1, mMaxFrames.mMax, &myLong)) {
-        mMaxFrames.mActual = myLong;
-
       } else if (GetBool(arg, "--show-dump-stats", &myBool)) {
         mShowDumpStats = myBool;
 
       } else if (strcmp(arg, "") == 0) {
         // This can only happen if there is trailing whitespace.  Ignore.
         MOZ_ASSERT(isEnd);
 
       } else {
@@ -1497,32 +1489,17 @@ Options::Options(const char* aDMDEnvVar)
   }
 }
 
 void
 Options::BadArg(const char* aArg)
 {
   StatusMsg("\n");
   StatusMsg("Bad entry in the $DMD environment variable: '%s'.\n", aArg);
-  StatusMsg("\n");
-  StatusMsg("$DMD must be a whitespace-separated list of |--option=val|\n");
-  StatusMsg("entries.\n");
-  StatusMsg("\n");
-  StatusMsg("The following options are allowed;  defaults are shown in [].\n");
-  StatusMsg("  --mode=<mode>                Profiling mode [dark-matter]\n");
-  StatusMsg("      where <mode> is one of: live, dark-matter, cumulative\n");
-  StatusMsg("  --sample-below=<1..%d> Sample blocks smaller than this [%d]\n",
-            int(mSampleBelowSize.mMax),
-            int(mSampleBelowSize.mDefault));
-  StatusMsg("                               (prime numbers are recommended)\n");
-  StatusMsg("  --max-frames=<1..%d>         Max. depth of stack traces [%d]\n",
-            int(mMaxFrames.mMax),
-            int(mMaxFrames.mDefault));
-  StatusMsg("  --show-dump-stats=<yes|no>   Show stats about dumps? [no]\n");
-  StatusMsg("\n");
+  StatusMsg("See the output of |mach help run| for the allowed options.\n");
   exit(1);
 }
 
 const char*
 Options::ModeString() const
 {
   switch (mMode) {
   case Live:
--- a/modules/libpref/init/all.js
+++ b/modules/libpref/init/all.js
@@ -5157,16 +5157,26 @@ pref("reader.font_type", "sans-serif");
 
 // Whether or not the user has interacted with the reader mode toolbar.
 // This is used to show a first-launch tip in reader mode.
 pref("reader.has_used_toolbar", false);
 
 // Whether to use a vertical or horizontal toolbar.
 pref("reader.toolbar.vertical", true);
 
+#if !defined(ANDROID)
+pref("narrate.enabled", true);
+#else
+pref("narrate.enabled", false);
+#endif
+
+pref("narrate.test", false);
+pref("narrate.rate", 0);
+pref("narrate.voice", "automatic");
+
 #if defined(XP_LINUX) && defined(MOZ_GMP_SANDBOX)
 // Whether to allow, on a Linux system that doesn't support the necessary sandboxing
 // features, loading Gecko Media Plugins unsandboxed.  However, EME CDMs will not be
 // loaded without sandboxing even if this pref is changed.
 pref("media.gmp.insecure.allow", false);
 #endif
 
 pref("dom.audiochannel.mutedByDefault", false);
--- a/python/mozbuild/mozbuild/mach_commands.py
+++ b/python/mozbuild/mozbuild/mach_commands.py
@@ -1113,23 +1113,20 @@ class RunProgram(MachCommandBase):
 
     @CommandArgumentGroup('DMD')
     @CommandArgument('--dmd', action='store_true', group='DMD',
         help='Enable DMD. The following arguments have no effect without this.')
     @CommandArgument('--mode', choices=['live', 'dark-matter', 'cumulative', 'scan'], group='DMD',
          help='Profiling mode. The default is \'dark-matter\'.')
     @CommandArgument('--sample-below', default=None, type=str, group='DMD',
         help='Sample blocks smaller than this. Use 1 for no sampling. The default is 4093.')
-    @CommandArgument('--max-frames', default=None, type=str, group='DMD',
-        help='The maximum depth of stack traces. The default and maximum is 24.')
     @CommandArgument('--show-dump-stats', action='store_true', group='DMD',
         help='Show stats when doing dumps.')
     def run(self, params, remote, background, noprofile, debug, debugger,
-        debugparams, slowscript, dmd, mode, sample_below, max_frames,
-        show_dump_stats):
+        debugparams, slowscript, dmd, mode, sample_below, show_dump_stats):
 
         if conditions.is_android(self):
             # Running Firefox for Android is completely different
             if dmd:
                 print("DMD is not supported for Firefox for Android")
                 return 1
             from mozrunner.devices.android_device import verify_android_device, run_firefox_for_android
             if not (debug or debugger or debugparams):
@@ -1202,18 +1199,16 @@ class RunProgram(MachCommandBase):
 
         if dmd:
             dmd_params = []
 
             if mode:
                 dmd_params.append('--mode=' + mode)
             if sample_below:
                 dmd_params.append('--sample-below=' + sample_below)
-            if max_frames:
-                dmd_params.append('--max-frames=' + max_frames)
             if show_dump_stats:
                 dmd_params.append('--show-dump-stats=yes')
 
             bin_dir = os.path.dirname(binpath)
             lib_name = self.substs['DLL_PREFIX'] + 'dmd' + self.substs['DLL_SUFFIX']
             dmd_lib = os.path.join(bin_dir, lib_name)
             if not os.path.exists(dmd_lib):
                 print("Please build with |--enable-dmd| to use DMD.")
--- a/toolkit/components/extensions/ext-webRequest.js
+++ b/toolkit/components/extensions/ext-webRequest.js
@@ -48,17 +48,17 @@ function WebRequestEventManager(context,
 
       // Fills in tabId typically.
       let result = {};
       extensions.emit("fill-browser-data", data.browser, data2, result);
       if (result.cancel) {
         return;
       }
 
-      let optional = ["requestHeaders", "responseHeaders", "statusCode", "redirectUrl"];
+      let optional = ["requestHeaders", "responseHeaders", "statusCode", "statusLine", "redirectUrl"];
       for (let opt of optional) {
         if (opt in data) {
           data2[opt] = data[opt];
         }
       }
 
       return runSafeSync(context, callback, data2);
     };
--- a/toolkit/components/extensions/test/mochitest/test_ext_webrequest.html
+++ b/toolkit/components/extensions/test/mochitest/test_ext_webrequest.html
@@ -91,16 +91,30 @@ function compareLists(list1, list2, kind
 function backgroundScript() {
   let checkCompleted = true;
   let savedTabId = -1;
 
   function shouldRecord(url) {
     return url.startsWith(BASE) || /^data:.*\bwebRequestTest\b/.test(url);
   }
 
+  let statuses = [
+    {url: /_script_good\b/, code: 200, line: /^HTTP\/1.1 200 OK\b/i},
+    {url: /\bredirection\b/, code: 302, line: /^HTTP\/1.1 302\b/},
+    {url: /\bnonexistent_script_/, code: 404, line: /^HTTP\/1.1 404 Not Found\b/i},
+  ];
+  function checkStatus(details) {
+    for (let {url, code, line} of statuses) {
+      if (url.test(details.url)) {
+        browser.test.assertTrue(code === details.statusCode, `HTTP status code ${code} for ${details.url} (found ${details.statusCode})`);
+        browser.test.assertTrue(line.test(details.statusLine), `HTTP status line ${line} for ${details.url} (found ${details.statusLine})`);
+      }
+    }
+  }
+
   function checkType(details) {
     let expected_type = "???";
     if (details.url.indexOf("style") != -1) {
       expected_type = "stylesheet";
     } else if (details.url.indexOf("image") != -1) {
       expected_type = "image";
     } else if (details.url.indexOf("script") != -1) {
       expected_type = "script";
@@ -297,16 +311,17 @@ function backgroundScript() {
     browser.test.log(`onBeforeRedirect ${details.url} -> ${details.redirectUrl}`);
     checkRequestId(details, "redirect");
     checkResourceType(details.type);
     if (shouldRecord(details.url)) {
       recorded.beforeRedirect.push(details.url);
 
       browser.test.assertEq(details.tabId, savedTabId, "correct tab ID");
       checkType(details);
+      checkStatus(details);
 
       let id = frameIDs.get(details.url);
       browser.test.assertEq(id, details.frameId, "frame ID same in onBeforeRedirect as onBeforeRequest");
       frameIDs.set(details.redirectUrl, details.frameId);
     }
     if (details.url.indexOf("_redirect.") != -1) {
       let expectedUrl = details.url.replace("_redirect.", "_good.");
       browser.test.assertEq(details.redirectUrl, expectedUrl, "correct redirectUrl value");
@@ -339,16 +354,17 @@ function backgroundScript() {
     }
     if (checkCompleted && !completedUrls[kind].has(details.url)) {
       // We can only tell IPs for HTTP requests.
       if (/^https?:/.test(details.url)) {
         browser.test.assertEq(details.ip, "127.0.0.1", "correct ip");
       }
       completedUrls[kind].add(details.url);
     }
+    checkStatus(details);
   }
 
   function onHeadersReceived(details) {
     checkIpAndRecord("headersReceived", details);
     processHeaders("response", details);
     browser.test.log(`After processing response headers: ${details.responseHeaders.toSource()}`);
     return {responseHeaders: details.responseHeaders};
   }
--- a/toolkit/components/moz.build
+++ b/toolkit/components/moz.build
@@ -29,16 +29,17 @@ DIRS += [
     'filepicker',
     'filewatcher',
     'finalizationwitness',
     'formautofill',
     'find',
     'gfx',
     'jsdownloads',
     'lz4',
+    'narrate',
     'mediasniffer',
     'microformats',
     'osfile',
     'parentalcontrols',
     'passwordmgr',
     'perf',
     'perfmonitoring',
     'places',
new file mode 100644
--- /dev/null
+++ b/toolkit/components/narrate/.eslintrc
@@ -0,0 +1,93 @@
+{
+  "extends": [
+    "../../.eslintrc"
+  ],
+
+  "globals": {
+    "Components": true,
+    "dump": true,
+    "Iterator": true
+  },
+
+  "env": { "browser": true },
+
+  "rules": {
+    // Mozilla stuff
+    "mozilla/no-aArgs": 1,
+    "mozilla/reject-importGlobalProperties": 1,
+    "mozilla/var-only-at-top-level": 1,
+
+    "block-scoped-var": 2,
+    "brace-style": [1, "1tbs", {"allowSingleLine": false}],
+    "camelcase": 1,
+    "comma-dangle": 1,
+    "comma-spacing": [1, {"before": false, "after": true}],
+    "comma-style": [1, "last"],
+    "complexity": 1,
+    "consistent-return": 2,
+    "curly": 2,
+    "dot-location": [1, "property"],
+    "dot-notation": 2,
+    "eol-last": 2,
+    "generator-star-spacing": [1, "after"],
+    "indent": [1, 2, {"SwitchCase": 1}],
+    "key-spacing": [1, {"beforeColon": false, "afterColon": true}],
+    "max-len": [1, 80, 2, {"ignoreUrls": true}],
+    "max-nested-callbacks": [2, 3],
+    "new-cap": [2, {"capIsNew": false}],
+    "new-parens": 2,
+    "no-array-constructor": 2,
+    "no-cond-assign": 2,
+    "no-control-regex": 2,
+    "no-debugger": 2,
+    "no-delete-var": 2,
+    "no-dupe-args": 2,
+    "no-dupe-keys": 2,
+    "no-duplicate-case": 2,
+    "no-else-return": 2,
+    "no-eval": 2,
+    "no-extend-native": 2,
+    "no-extra-bind": 2,
+    "no-extra-boolean-cast": 2,
+    "no-extra-semi": 1,
+    "no-fallthrough": 2,
+    "no-inline-comments": 1,
+    "no-lonely-if": 2,
+    "no-mixed-spaces-and-tabs": 2,
+    "no-multi-spaces": 1,
+    "no-multi-str": 1,
+    "no-multiple-empty-lines": [1, {"max": 1}],
+    "no-native-reassign": 2,
+    "no-nested-ternary": 2,
+    "no-redeclare": 2,
+    "no-return-assign": 2,
+    "no-self-compare": 2,
+    "no-sequences": 2,
+    "no-shadow": 1,
+    "no-shadow-restricted-names": 2,
+    "no-spaced-func": 1,
+    "no-throw-literal": 2,
+    "no-trailing-spaces": 2,
+    "no-undef": 2,
+    "no-unneeded-ternary": 2,
+    "no-unreachable": 2,
+    "no-unused-vars": 2,
+    "no-with": 2,
+    "padded-blocks": [1, "never"],
+    "quotes": [1, "double", "avoid-escape"],
+    "semi": [1, "always"],
+    "semi-spacing": [1, {"before": false, "after": true}],
+    "space-after-keywords": [1, "always"],
+    "space-before-blocks": [1, "always"],
+    "space-before-function-paren": [1, "never"],
+    "space-in-parens": [1, "never"],
+    "space-infix-ops": [1, {"int32Hint": true}],
+    "space-return-throw-case": 1,
+    "space-unary-ops": [1, { "words": true, "nonwords": false }],
+    "spaced-comment": [1, "always"],
+    "strict": [2, "global"],
+    "use-isnan": 2,
+    "valid-typeof": 2,
+    "yoda": 2
+  }
+}
new file mode 100644
--- /dev/null
+++ b/toolkit/components/narrate/NarrateControls.jsm
@@ -0,0 +1,244 @@
+/* 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/. */
+
+"use strict";
+
+const Cu = Components.utils;
+
+Cu.import("resource://gre/modules/narrate/VoiceSelect.jsm");
+Cu.import("resource://gre/modules/narrate/Narrator.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+
+this.EXPORTED_SYMBOLS = ["NarrateControls"];
+
+var gStrings = Services.strings.createBundle("chrome://global/locale/narrate.properties");
+
+function NarrateControls(mm, win) {
+  this._mm = mm;
+  this._winRef = Cu.getWeakReference(win);
+
+  // Append content style sheet in document head
+  let style = win.document.createElement("link");
+  style.rel = "stylesheet";
+  style.href = "chrome://global/skin/narrate.css";
+  win.document.head.appendChild(style);
+
+  function localize(pieces, ...substitutions) {
+    let result = pieces[0];
+    for (let i = 0; i < substitutions.length; ++i) {
+      result += gStrings.GetStringFromName(substitutions[i]) + pieces[i + 1];
+    }
+    return result;
+  }
+
+  let dropdown = win.document.createElement("ul");
+  dropdown.className = "dropdown";
+  dropdown.id = "narrate-dropdown";
+  dropdown.innerHTML =
+    localize`<style scoped>
+      @import url("chrome://global/skin/narrateControls.css");
+    </style>
+    <li>
+       <button class="dropdown-toggle button"
+               id="narrate-toggle" title="${"narrate"}"></button>
+    </li>
+    <li class="dropdown-popup">
+      <div id="narrate-control" class="narrate-row">
+        <button disabled id="narrate-skip-previous"
+                title="${"back"}"></button>
+        <button id="narrate-start-stop" title="${"start"}"></button>
+        <button disabled id="narrate-skip-next"
+                title="${"forward"}"></button>
+      </div>
+      <div id="narrate-rate" class="narrate-row">
+        <input id="narrate-rate-input" value="0" title="${"speed"}"
+               step="25" max="400" min="-400" type="range">
+      </div>
+      <div id="narrate-voices" class="narrate-row"></div>
+      <div class="dropdown-arrow"></div>
+    </li>`;
+
+  this.narrator = new Narrator(win);
+
+  let selectLabel = gStrings.GetStringFromName("selectvoicelabel");
+  let comparer = win.Intl ?
+    (new Intl.Collator()).compare : (a, b) => a.localeCompare(b);
+  let options = this.narrator.getVoiceOptions().map(v => {
+    return {
+      label: this._createVoiceLabel(v),
+      value: v.voiceURI
+    };
+  }).sort((a, b) => comparer(a.label, b.label));
+  options.unshift({
+    label: gStrings.GetStringFromName("defaultvoice"),
+    value: "automatic"
+  });
+  this.voiceSelect = new VoiceSelect(win, selectLabel, options);
+  this.voiceSelect.element.addEventListener("change", this);
+  this.voiceSelect.element.id = "voice-select";
+  dropdown.querySelector("#narrate-voices").appendChild(
+    this.voiceSelect.element);
+
+  dropdown.addEventListener("click", this, true);
+
+  let rateRange = dropdown.querySelector("#narrate-rate > input");
+  rateRange.addEventListener("input", this);
+  rateRange.addEventListener("mousedown", this);
+  rateRange.addEventListener("mouseup", this);
+
+  let branch = Services.prefs.getBranch("narrate.");
+  this.voiceSelect.value = branch.getCharPref("voice");
+  // The rate is stored as an integer.
+  rateRange.value = branch.getIntPref("rate");
+
+  let tb = win.document.getElementById("reader-toolbar");
+  tb.appendChild(dropdown);
+}
+
+NarrateControls.prototype = {
+  handleEvent: function(evt) {
+    switch (evt.type) {
+      case "mousedown":
+        this._rateMousedown = true;
+        break;
+      case "mouseup":
+        this._rateMousedown = false;
+        break;
+      case "input":
+        this._onRateInput(evt);
+        break;
+      case "change":
+        this._onVoiceChange();
+        break;
+      case "click":
+        this._onButtonClick(evt);
+        break;
+    }
+  },
+
+  _onRateInput: function(evt) {
+    if (!this._rateMousedown) {
+      this._mm.sendAsyncMessage("Reader:SetIntPref",
+        { name: "narrate.rate", value: evt.target.value });
+      this.narrator.setRate(this._convertRate(evt.target.value));
+    }
+  },
+
+  _onVoiceChange: function() {
+    let voice = this.voice;
+    this._mm.sendAsyncMessage("Reader:SetCharPref",
+      { name: "narrate.voice", value: voice });
+    this.narrator.setVoice(voice);
+  },
+
+  _onButtonClick: function(evt) {
+    switch (evt.target.id) {
+      case "narrate-skip-previous":
+        this.narrator.skipPrevious();
+        break;
+      case "narrate-skip-next":
+        this.narrator.skipNext();
+        break;
+      case "narrate-start-stop":
+        if (this.narrator.speaking) {
+          this.narrator.stop();
+        } else {
+          this._updateSpeechControls(true);
+          let options = { rate: this.rate, voice: this.voice };
+          this.narrator.start(options).then(() => {
+            this._updateSpeechControls(false);
+          });
+        }
+        break;
+      case "narrate-toggle":
+        let dropdown = this._doc.getElementById("narrate-dropdown");
+        if (dropdown.classList.contains("open")) {
+          if (this.narrator.speaking) {
+            this.narrator.stop();
+          }
+
+          // We need to remove "keep-open" class here so that AboutReader
+          // closes this dropdown properly. This class is eventually removed in
+          // _updateSpeechControls which gets called after narration stops,
+          // but that happend asynchronously and is too late.
+          dropdown.classList.remove("keep-open");
+        }
+        break;
+    }
+  },
+
+  _updateSpeechControls: function(speaking) {
+    let dropdown = this._doc.getElementById("narrate-dropdown");
+    dropdown.classList.toggle("keep-open", speaking);
+
+    let startStopButton = this._doc.getElementById("narrate-start-stop");
+    startStopButton.classList.toggle("speaking", speaking);
+    startStopButton.title =
+      gStrings.GetStringFromName(speaking ? "start" : "stop");
+
+    this._doc.getElementById("narrate-skip-previous").disabled = !speaking;
+    this._doc.getElementById("narrate-skip-next").disabled = !speaking;
+  },
+
+  _createVoiceLabel: function(voice) {
+    // This is a highly imperfect method of making human-readable labels
+    // for system voices. Because each platform has a different naming scheme
+    // for voices, we use a different method for each platform.
+    switch (Services.appinfo.OS) {
+      case "WINNT":
+        // On windows the language is included in the name, so just use the name
+        return voice.name;
+      case "Linux":
+        // On Linux, the name is usually the unlocalized language name.
+        // Use a localized language name, and have the language tag in
+        // parenthisis. This is to avoid six languages called "English".
+        return gStrings.formatStringFromName("voiceLabel",
+          [this._getLanguageName(voice.lang) || voice.name, voice.lang], 2);
+      default:
+        // On Mac the language is not included in the name, find a localized
+        // language name or show the tag if none exists.
+        // This is the ideal naming scheme so it is also the "default".
+        return gStrings.formatStringFromName("voiceLabel",
+          [voice.name, this._getLanguageName(voice.lang) || voice.lang], 2);
+    }
+  },
+
+  _getLanguageName: function(lang) {
+    if (!this._langStrings) {
+      this._langStrings = Services.strings.createBundle(
+        "chrome://global/locale/languageNames.properties ");
+    }
+
+    try {
+      // language tags will be lower case ascii between 2 and 3 characters long.
+      return this._langStrings.GetStringFromName(lang.match(/^[a-z]{2,3}/)[0]);
+    } catch (e) {
+      return "";
+    }
+  },
+
+  _convertRate: function(rate) {
+    // We need to convert a relative percentage value to a fraction rate value.
+    // eg. -100 is half the speed, 100 is twice the speed in percentage,
+    // 0.5 is half the speed and 2 is twice the speed in fractions.
+    return Math.pow(Math.abs(rate / 100) + 1, rate < 0 ? -1 : 1);
+  },
+
+  get _win() {
+    return this._winRef.get();
+  },
+
+  get _doc() {
+    return this._win.document;
+  },
+
+  get rate() {
+    return this._convertRate(
+      this._doc.getElementById("narrate-rate-input").value);
+  },
+
+  get voice() {
+    return this.voiceSelect.value;
+  }
+};
new file mode 100644
--- /dev/null
+++ b/toolkit/components/narrate/Narrator.jsm
@@ -0,0 +1,219 @@
+/* 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/. */
+
+"use strict";
+
+const { interfaces: Ci, utils: Cu } = Components;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "LanguageDetector",
+  "resource:///modules/translation/LanguageDetector.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Services",
+  "resource://gre/modules/Services.jsm");
+
+this.EXPORTED_SYMBOLS = [ "Narrator" ];
+
+// Maximum time into paragraph when pressing "skip previous" will go
+// to previous paragraph and not the start of current one.
+const PREV_THRESHOLD = 2000;
+
+function Narrator(win) {
+  this._winRef = Cu.getWeakReference(win);
+  this._inTest = Services.prefs.getBoolPref("narrate.test");
+  this._speechOptions = {};
+  this._startTime = 0;
+  this._stopped = false;
+}
+
+Narrator.prototype = {
+  get _doc() {
+    return this._winRef.get().document;
+  },
+
+  get _win() {
+    return this._winRef.get();
+  },
+
+  get _voiceMap() {
+    if (!this._voiceMapInner) {
+      this._voiceMapInner = new Map();
+      for (let voice of this._win.speechSynthesis.getVoices()) {
+        this._voiceMapInner.set(voice.voiceURI, voice);
+      }
+    }
+
+    return this._voiceMapInner;
+  },
+
+  get _paragraphs() {
+    if (!this._paragraphsInner) {
+      let wu = this._win.QueryInterface(
+        Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils);
+      let queryString = "#reader-header > *:not(style):not(:empty), " +
+        "#moz-reader-content > .page > * > *:not(style):not(:empty)";
+      // filter out zero sized paragraphs.
+      let paragraphs = Array.from(this._doc.querySelectorAll(queryString));
+      paragraphs = paragraphs.filter(p => {
+        let bb = wu.getBoundsWithoutFlushing(p);
+        return bb.width && bb.height;
+      });
+
+      this._paragraphsInner = paragraphs.map(Cu.getWeakReference);
+    }
+
+    return this._paragraphsInner;
+  },
+
+  get _timeIntoParagraph() {
+    let rv = Date.now() - this._startTime;
+    return rv;
+  },
+
+  get speaking() {
+    return this._win.speechSynthesis.speaking ||
+      this._win.speechSynthesis.pending;
+  },
+
+  _getParagraphAt: function(index) {
+    let paragraph = this._paragraphsInner[index];
+    return paragraph ? paragraph.get() : null;
+  },
+
+  _isParagraphInView: function(paragraphRef) {
+    let paragraph = paragraphRef && paragraphRef.get && paragraphRef.get();
+    if (!paragraph) {
+      return false;
+    }
+
+    let bb = paragraph.getBoundingClientRect();
+    return bb.top >= 0 && bb.top < this._win.innerHeight;
+  },
+
+  _detectLanguage: function() {
+    if (this._speechOptions.lang || this._speechOptions.voice) {
+      return Promise.resolve();
+    }
+
+    let sampleText = this._doc.getElementById(
+      "moz-reader-content").textContent.substring(0, 60 * 1024);
+    return LanguageDetector.detectLanguage(sampleText).then(result => {
+      if (result.confident) {
+        this._speechOptions.lang = result.language;
+      }
+    });
+  },
+
+  _sendTestEvent: function(eventType, detail) {
+    let win = this._win;
+    win.dispatchEvent(new win.CustomEvent(eventType,
+      { detail: Cu.cloneInto(detail, win.document) }));
+  },
+
+  _speakInner: function() {
+    this._win.speechSynthesis.cancel();
+    let paragraph = this._getParagraphAt(this._index);
+    let utterance = new this._win.SpeechSynthesisUtterance(
+      paragraph.textContent);
+    utterance.rate = this._speechOptions.rate;
+    if (this._speechOptions.voice) {
+      utterance.voice = this._speechOptions.voice;
+    } else {
+      utterance.lang = this._speechOptions.lang;
+    }
+
+    this._startTime = Date.now();
+
+    return new Promise(resolve => {
+      utterance.addEventListener("start", () => {
+        paragraph.classList.add("narrating");
+        let bb = paragraph.getBoundingClientRect();
+        if (bb.top < 0 || bb.bottom > this._win.innerHeight) {
+          paragraph.scrollIntoView({ behavior: "smooth", block: "start"});
+        }
+
+        if (this._inTest) {
+          this._sendTestEvent("paragraphstart", {
+            voice: utterance.chosenVoiceURI,
+            rate: utterance.rate,
+            paragraph: this._index
+          });
+        }
+      });
+
+      utterance.addEventListener("end", () => {
+        if (!this._win) {
+          // page got unloaded, don't do anything.
+          return;
+        }
+
+        paragraph.classList.remove("narrating");
+        this._startTime = 0;
+        if (this._inTest) {
+          this._sendTestEvent("paragraphend", {});
+        }
+
+        if (this._index + 1 >= this._paragraphs.length || this._stopped) {
+          // We reached the end of the document, or the user pressed stopped.
+          resolve();
+        } else {
+          this._index++;
+          this._speakInner().then(resolve);
+        }
+      });
+
+      this._win.speechSynthesis.speak(utterance);
+    });
+  },
+
+  getVoiceOptions: function() {
+    return Array.from(this._voiceMap.values());
+  },
+
+  start: function(speechOptions) {
+    this._speechOptions = {
+      rate: speechOptions.rate,
+      voice: this._voiceMap.get(speechOptions.voice)
+    };
+
+    this._stopped = false;
+    return this._detectLanguage().then(() => {
+      if (!this._isParagraphInView(this._paragraphs[this._index])) {
+        this._index = this._paragraphs.findIndex(
+          this._isParagraphInView.bind(this));
+      }
+
+      return this._speakInner();
+    });
+  },
+
+  stop: function() {
+    this._stopped = true;
+    this._win.speechSynthesis.cancel();
+  },
+
+  skipNext: function() {
+    this._win.speechSynthesis.cancel();
+  },
+
+  skipPrevious: function() {
+    this._index -=
+      this._index > 0 && this._timeIntoParagraph < PREV_THRESHOLD ? 2 : 1;
+    this._win.speechSynthesis.cancel();
+  },
+
+  setRate: function(rate) {
+    this._speechOptions.rate = rate;
+    /* repeat current paragraph */
+    this._index--;
+    this._win.speechSynthesis.cancel();
+  },
+
+  setVoice: function(voice) {
+    this._speechOptions.voice = this._voiceMap.get(voice);
+    /* repeat current paragraph */
+    this._index--;
+    this._win.speechSynthesis.cancel();
+  }
+};
new file mode 100644
--- /dev/null
+++ b/toolkit/components/narrate/VoiceSelect.jsm
@@ -0,0 +1,291 @@
+/* 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/. */
+
+"use strict";
+
+const Cu = Components.utils;
+
+this.EXPORTED_SYMBOLS = ["VoiceSelect"];
+
+function VoiceSelect(win, label, options = []) {
+  this._winRef = Cu.getWeakReference(win);
+
+  let element = win.document.createElement("div");
+  element.classList.add("voiceselect");
+  element.innerHTML =
+   `<button class="select-toggle" aria-controls="voice-options">
+      <span class="label">${label}</span> <span class="current-voice"></span>
+    </button>
+    <div class="options" id="voice-options" role="listbox"></div>`;
+
+  this._elementRef = Cu.getWeakReference(element);
+
+  let button = this.selectToggle;
+  button.addEventListener("click", this);
+  button.addEventListener("keypress", this);
+
+  let listbox = this.listbox;
+  listbox.addEventListener("click", this);
+  listbox.addEventListener("mousemove", this);
+  listbox.addEventListener("keypress", this);
+  listbox.addEventListener("wheel", this, true);
+
+  win.addEventListener("resize", () => {
+    this._updateDropdownHeight();
+  });
+
+  for (let option of options) {
+    this.add(option.label, option.value);
+  }
+
+  this.selectedIndex = 0;
+}
+
+VoiceSelect.prototype = {
+  add: function(label, value) {
+    let option = this._doc.createElement("button");
+    option.dataset.value = value;
+    option.classList.add("option");
+    option.tabIndex = "-1";
+    option.setAttribute("role", "option");
+    option.textContent = label;
+    this.listbox.appendChild(option);
+  },
+
+  toggleList: function(force, focus = true) {
+    if (this.element.classList.toggle("open", force)) {
+      if (focus) {
+        (this.selected || this.options[0]).focus();
+      }
+
+      this._updateDropdownHeight(true);
+      this.listbox.setAttribute("aria-expanded", true);
+      this._win.addEventListener("focus", this, true);
+    } else {
+      if (focus) {
+        this.element.querySelector(".select-toggle").focus();
+      }
+
+      this.listbox.setAttribute("aria-expanded", false);
+      this._win.removeEventListener("focus", this, true);
+    }
+  },
+
+  handleEvent: function(evt) {
+    let target = evt.target;
+
+    switch (evt.type) {
+      case "click":
+        if (target.classList.contains("option")) {
+          if (!target.classList.contains("selected")) {
+            this.selected = target;
+          }
+
+          this.toggleList(false);
+        } else if (target.classList.contains("select-toggle")) {
+          this.toggleList();
+        }
+        break;
+
+      case "mousemove":
+        this.listbox.classList.add("hovering");
+        break;
+
+      case "keypress":
+        if (target.classList.contains("select-toggle")) {
+          if (evt.altKey) {
+            this.toggleList(true);
+          } else {
+            this._keyPressedButton(evt);
+          }
+        } else {
+          this.listbox.classList.remove("hovering");
+          this._keyPressedInBox(evt);
+        }
+        break;
+
+      case "wheel":
+        // Don't let wheel events bubble to document. It will scroll the page
+        // and close the entire narrate dialog.
+        evt.stopPropagation();
+        break;
+
+      case "focus":
+        this._win.console.log(evt);
+        if (!evt.target.closest('.options')) {
+          this.toggleList(false, false);
+        }
+        break;
+    }
+  },
+
+  _getPagedOption: function(option, up) {
+    let height = elem => elem.getBoundingClientRect().height;
+    let listboxHeight = height(this.listbox);
+
+    let next = option;
+    for (let delta = 0; delta < listboxHeight; delta += height(next)) {
+      let sibling = up ? next.previousElementSibling : next.nextElementSibling;
+      if (!sibling) {
+        break;
+      }
+
+      next = sibling;
+    }
+
+    return next;
+  },
+
+  _keyPressedButton: function(evt) {
+    if (evt.altKey && (evt.key === "ArrowUp" || evt.key === "ArrowUp")) {
+      this.toggleList(true);
+      return;
+    }
+
+    let toSelect;
+    switch (evt.key) {
+      case "PageUp":
+      case "ArrowUp":
+        toSelect = this.selected.previousElementSibling;
+        break;
+      case "PageDown":
+      case "ArrowDown":
+        toSelect = this.selected.nextElementSibling;
+        break;
+      case "Home":
+        toSelect = this.selected.parentNode.firstElementChild;
+        break;
+      case "End":
+        toSelect = this.selected.parentNode.lastElementChild;
+        break;
+    }
+
+    if (toSelect && toSelect.classList.contains("option")) {
+      evt.preventDefault();
+      this.selected = toSelect;
+    }
+  },
+
+  _keyPressedInBox: function(evt) {
+    let toFocus;
+    let cur = this._doc.activeElement;
+
+    switch (evt.key) {
+      case "ArrowUp":
+        toFocus = cur.previousElementSibling || this.listbox.lastElementChild;
+        break;
+      case "ArrowDown":
+        toFocus = cur.nextElementSibling || this.listbox.firstElementChild;
+        break;
+      case "PageUp":
+        toFocus = this._getPagedOption(cur, true);
+        break;
+      case "PageDown":
+        toFocus = this._getPagedOption(cur, false);
+        break;
+      case "Home":
+        toFocus = cur.parentNode.firstElementChild;
+        break;
+      case "End":
+        toFocus = cur.parentNode.lastElementChild;
+        break;
+      case "Escape":
+        this.toggleList(false);
+        break;
+    }
+
+    if (toFocus && toFocus.classList.contains("option")) {
+      evt.preventDefault();
+      toFocus.focus();
+    }
+  },
+
+  _select: function(option) {
+    let oldSelected = this.selected;
+    if (oldSelected) {
+      oldSelected.removeAttribute("aria-selected");
+      oldSelected.classList.remove("selected");
+    }
+
+    if (option) {
+      option.setAttribute("aria-selected", true);
+      option.classList.add("selected");
+      this.element.querySelector(".current-voice").textContent =
+        option.textContent;
+    }
+
+    let evt = this.element.ownerDocument.createEvent("Event");
+    evt.initEvent("change", true, true);
+    this.element.dispatchEvent(evt);
+  },
+
+  _updateDropdownHeight: function(now) {
+    let updateInner = () => {
+      let winHeight = this._win.innerHeight;
+      let listbox = this.listbox;
+      let listboxTop = listbox.getBoundingClientRect().top;
+      listbox.style.maxHeight = (winHeight - listboxTop - 10) + "px";
+    };
+
+    if (now) {
+      updateInner();
+    } else if (!this._pendingDropdownUpdate) {
+      this._pendingDropdownUpdate = true;
+      this._win.requestAnimationFrame(() => {
+        updateInner();
+        delete this._pendingDropdownUpdate;
+      });
+    }
+  },
+
+  get element() {
+    return this._elementRef.get();
+  },
+
+  get listbox() {
+    return this._elementRef.get().querySelector(".options");
+  },
+
+  get selectToggle() {
+    return this._elementRef.get().querySelector(".select-toggle");
+  },
+
+  get _win() {
+    return this._winRef.get();
+  },
+
+  get _doc() {
+    return this._win.document;
+  },
+
+  set selected(option) {
+    this._select(option);
+  },
+
+  get selected() {
+    return this.element.querySelector(".options > .option.selected");
+  },
+
+  get options() {
+    return this.element.querySelectorAll(".options > .option");
+  },
+
+  set selectedIndex(index) {
+    this._select(this.options[index]);
+  },
+
+  get selectedIndex() {
+    return Array.from(this.options).indexOf(this.selected);
+  },
+
+  set value(value) {
+    let option = Array.from(this.options).find(o => o.dataset.value === value);
+    this._select(option);
+  },
+
+  get value() {
+    let selected = this.selected;
+    return selected ? selected.dataset.value : "";
+  }
+};
new file mode 100644
--- /dev/null
+++ b/toolkit/components/narrate/moz.build
@@ -0,0 +1,13 @@
+# -*- Mode: python; c-basic-offset: 4; 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/.
+
+EXTRA_JS_MODULES.narrate = [
+  'NarrateControls.jsm',
+  'Narrator.jsm',
+  'VoiceSelect.jsm'
+]
+
+BROWSER_CHROME_MANIFESTS += ['test/browser.ini']
new file mode 100644
--- /dev/null
+++ b/toolkit/components/narrate/test/.eslintrc
@@ -0,0 +1,21 @@
+{
+  "extends": [
+    "../.eslintrc"
+  ],
+
+  "globals": {
+    "is": true,
+    "isnot": true,
+    "ok": true,
+    "NarrateTestUtils": true,
+    "content": true,
+    "ContentTaskUtils": true,
+    "ContentTask": true,
+    "BrowserTestUtils": true,
+    "gBrowser": true,
+  },
+
+  "rules": {
+    "mozilla/import-headjs-globals": 1
+  }
+}
new file mode 100644
--- /dev/null
+++ b/toolkit/components/narrate/test/NarrateTestUtils.jsm
@@ -0,0 +1,114 @@
+/* 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/. */
+
+"use strict";
+
+Components.utils.import("resource://gre/modules/Services.jsm");
+
+this.EXPORTED_SYMBOLS = [ "NarrateTestUtils" ];
+
+this.NarrateTestUtils = {
+  TOGGLE: "#narrate-toggle",
+  POPUP: "#narrate-dropdown .dropdown-popup",
+  VOICE_SELECT: "#narrate-voices .select-toggle",
+  VOICE_OPTIONS: "#narrate-voices .options",
+  VOICE_SELECTED: "#narrate-voices .options .option.selected",
+  VOICE_SELECT_LABEL: "#narrate-voices .select-toggle .current-voice",
+  RATE: "#narrate-rate-input",
+  START: "#narrate-start-stop:not(.speaking)",
+  STOP: "#narrate-start-stop.speaking",
+  BACK: "#narrate-skip-previous",
+  FORWARD: "#narrate-skip-next",
+
+  isVisible: function(element) {
+    let style = element.ownerDocument.defaultView.getComputedStyle(element, "");
+    if (style.display == "none") {
+      return false;
+    } else if (style.visibility != "visible") {
+      return false;
+    } else if (style.display == "-moz-popup" && element.state != "open") {
+      return false;
+    }
+
+    // Hiding a parent element will hide all its children
+    if (element.parentNode != element.ownerDocument) {
+      return this.isVisible(element.parentNode);
+    }
+
+    return true;
+  },
+
+  isStoppedState: function(window, ok) {
+    let $ = window.document.querySelector.bind(window.document);
+    ok($(this.BACK).disabled, "back button is disabled");
+    ok($(this.FORWARD).disabled, "forward button is disabled");
+    ok(!!$(this.START), "start button is showing");
+    ok(!$(this.STOP), "stop button is hidden");
+  },
+
+  isStartedState: function(window, ok) {
+    let $ = window.document.querySelector.bind(window.document);
+    ok(!$(this.BACK).disabled, "back button is enabled");
+    ok(!$(this.FORWARD).disabled, "forward button is enabled");
+    ok(!$(this.START), "start button is hidden");
+    ok(!!$(this.STOP), "stop button is showing");
+  },
+
+  selectVoice: function(window, voiceUri) {
+    if (!this.isVisible(window.document.querySelector(this.VOICE_OPTIONS))) {
+      window.document.querySelector(this.VOICE_SELECT).click();
+    }
+
+    let voiceOption = window.document.querySelector(
+      `#narrate-voices .option[data-value="${voiceUri}"]`);
+
+    voiceOption.focus();
+    voiceOption.click();
+
+    return voiceOption.classList.contains("selected");
+  },
+
+  getEventUtils: function(window) {
+    let eventUtils = {
+      "_EU_Ci": Components.interfaces,
+      "_EU_Cc": Components.classes,
+      window: window,
+      parent: window,
+      navigator: window.navigator,
+      KeyboardEvent: window.KeyboardEvent,
+      KeyEvent: window.KeyEvent
+    };
+    Services.scriptloader.loadSubScript(
+      "chrome://mochikit/content/tests/SimpleTest/EventUtils.js", eventUtils);
+    return eventUtils;
+  },
+
+  getReaderReadyPromise: function(window) {
+    return new Promise(resolve => {
+      function observeReady(subject, topic) {
+        if (subject == window) {
+          Services.obs.removeObserver(observeReady, topic);
+          resolve();
+        }
+      }
+
+      if (window.document.body.classList.contains("loaded")) {
+        resolve();
+      } else {
+        Services.obs.addObserver(observeReady, "AboutReader:Ready", false);
+      }
+    });
+  },
+
+  waitForPrefChange: function(pref) {
+    return new Promise(resolve => {
+      function observeChange() {
+        Services.prefs.removeObserver(pref, observeChange);
+        resolve();
+      }
+
+      Services.prefs.addObserver(pref, observeChange, false);
+    });
+  }
+};
new file mode 100644
--- /dev/null
+++ b/toolkit/components/narrate/test/browser.ini
@@ -0,0 +1,8 @@
+[DEFAULT]
+support-files =
+  head.js
+  NarrateTestUtils.jsm
+
+[browser_narrate.js]
+[browser_narrate_disable.js]
+[browser_voiceselect.js]
new file mode 100644
--- /dev/null
+++ b/toolkit/components/narrate/test/browser_narrate.js
@@ -0,0 +1,101 @@
+/* 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/. */
+
+/* globals is, isnot, registerCleanupFunction, add_task */
+
+"use strict";
+
+registerCleanupFunction(teardown);
+
+add_task(function* testNarrate() {
+  setup();
+
+  yield spawnInNewReaderTab(TEST_ARTICLE, function* () {
+    let TEST_VOICE = "urn:moz-tts:fake-indirect:teresa";
+    let $ = content.document.querySelector.bind(content.document);
+
+    let popup = $(NarrateTestUtils.POPUP);
+    ok(!NarrateTestUtils.isVisible(popup), "popup is initially hidden");
+
+    let toggle = $(NarrateTestUtils.TOGGLE);
+    toggle.click();
+
+    ok(NarrateTestUtils.isVisible(popup), "popup toggled");
+
+    let voiceOptions = $(NarrateTestUtils.VOICE_OPTIONS);
+    ok(!NarrateTestUtils.isVisible(voiceOptions),
+      "voice options are initially hidden");
+
+    $(NarrateTestUtils.VOICE_SELECT).click();
+    ok(NarrateTestUtils.isVisible(voiceOptions), "voice options pop up");
+
+    let prefChanged = NarrateTestUtils.waitForPrefChange("narrate.voice");
+    ok(NarrateTestUtils.selectVoice(content, TEST_VOICE),
+      "test voice selected");
+    yield prefChanged;
+
+    ok(!NarrateTestUtils.isVisible(voiceOptions), "voice options hidden again");
+
+    NarrateTestUtils.isStoppedState(content, ok);
+
+    let promiseEvent = ContentTaskUtils.waitForEvent(content, "paragraphstart");
+    $(NarrateTestUtils.START).click();
+    let speechinfo = (yield promiseEvent).detail;
+    is(speechinfo.voice, TEST_VOICE, "correct voice is being used");
+    is(speechinfo.paragraph, 0, "first paragraph is being spoken");
+
+    NarrateTestUtils.isStartedState(content, ok);
+
+    promiseEvent = ContentTaskUtils.waitForEvent(content, "paragraphstart");
+    $(NarrateTestUtils.FORWARD).click();
+    speechinfo = (yield promiseEvent).detail;
+    is(speechinfo.voice, TEST_VOICE, "same voice is used");
+    is(speechinfo.paragraph, 1, "second paragraph is being spoken");
+
+    NarrateTestUtils.isStartedState(content, ok);
+
+    let eventUtils = NarrateTestUtils.getEventUtils(content);
+
+    promiseEvent = ContentTaskUtils.waitForEvent(content, "paragraphstart");
+    prefChanged = NarrateTestUtils.waitForPrefChange("narrate.rate");
+    $(NarrateTestUtils.RATE).focus();
+    eventUtils.sendKey("PAGE_UP", content);
+    let newspeechinfo = (yield promiseEvent).detail;
+    is(newspeechinfo.paragraph, speechinfo.paragraph, "same paragraph");
+    isnot(newspeechinfo.rate, speechinfo.rate, "rate changed");
+    yield prefChanged;
+
+    promiseEvent = ContentTaskUtils.waitForEvent(content, "paragraphend");
+    $(NarrateTestUtils.STOP).click();
+    yield promiseEvent;
+
+    yield ContentTaskUtils.waitForCondition(
+      () => !$(NarrateTestUtils.STOP), "transitioned to stopped state");
+    NarrateTestUtils.isStoppedState(content, ok);
+
+    promiseEvent = ContentTaskUtils.waitForEvent(content, "scroll");
+    content.scrollBy(0, 10);
+    yield promiseEvent;
+    ok(!NarrateTestUtils.isVisible(popup), "popup is hidden after scroll");
+
+    toggle.click();
+    ok(NarrateTestUtils.isVisible(popup), "popup is toggled again");
+
+    promiseEvent = ContentTaskUtils.waitForEvent(content, "paragraphstart");
+    $(NarrateTestUtils.START).click();
+    yield promiseEvent;
+    NarrateTestUtils.isStartedState(content, ok);
+
+    promiseEvent = ContentTaskUtils.waitForEvent(content, "scroll");
+    content.scrollBy(0, -10);
+    yield promiseEvent;
+    ok(NarrateTestUtils.isVisible(popup), "popup stays visible after scroll");
+
+    promiseEvent = ContentTaskUtils.waitForEvent(content, "paragraphend");
+    toggle.click();
+    yield promiseEvent;
+    ok(!NarrateTestUtils.isVisible(popup), "popup is dismissed while speaking");
+    ok(true, "speech stopped when popup is dismissed");
+  });
+});
new file mode 100644
--- /dev/null
+++ b/toolkit/components/narrate/test/browser_narrate_disable.js
@@ -0,0 +1,37 @@
+/* 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/. */
+
+/* globals registerCleanupFunction, add_task */
+
+"use strict";
+
+const ENABLE_PREF = "narrate.enabled";
+
+registerCleanupFunction(() => {
+  clearUserPref(ENABLE_PREF);
+  teardown();
+});
+
+add_task(function* testNarratePref() {
+  setup();
+
+  yield spawnInNewReaderTab(TEST_ARTICLE, function() {
+    is(content.document.querySelectorAll(NarrateTestUtils.TOGGLE).length, 1,
+      "narrate is inserted by default");
+  });
+
+  setBoolPref(ENABLE_PREF, false);
+
+  yield spawnInNewReaderTab(TEST_ARTICLE, function() {
+    ok(!content.document.querySelector(NarrateTestUtils.TOGGLE),
+      "narrate is disabled and is not in reader mode");
+  });
+
+  setBoolPref(ENABLE_PREF, true);
+
+  yield spawnInNewReaderTab(TEST_ARTICLE, function() {
+    is(content.document.querySelectorAll(NarrateTestUtils.TOGGLE).length, 1,
+      "narrate is re-enabled and appears only once");
+  });
+});
new file mode 100644
--- /dev/null
+++ b/toolkit/components/narrate/test/browser_voiceselect.js
@@ -0,0 +1,106 @@
+/* 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/. */
+
+/* globals registerCleanupFunction, add_task, is, isnot */
+
+"use strict";
+
+registerCleanupFunction(teardown);
+
+add_task(function* testVoiceselectDropdownAutoclose() {
+  setup();
+
+  yield spawnInNewReaderTab(TEST_ARTICLE, function* () {
+    let $ = content.document.querySelector.bind(content.document);
+
+    $(NarrateTestUtils.TOGGLE).click();
+    ok(NarrateTestUtils.isVisible($(NarrateTestUtils.POPUP)),
+      "popup is toggled");
+
+    ok(!NarrateTestUtils.isVisible($(NarrateTestUtils.VOICE_OPTIONS)),
+      "voice options are initially hidden");
+
+    $(NarrateTestUtils.VOICE_SELECT).click();
+    ok(NarrateTestUtils.isVisible($(NarrateTestUtils.VOICE_OPTIONS)),
+      "voice options are toggled");
+
+    $(NarrateTestUtils.TOGGLE).click();
+    // A focus will follow a real click.
+    $(NarrateTestUtils.TOGGLE).focus();
+    ok(!NarrateTestUtils.isVisible($(NarrateTestUtils.POPUP)),
+      "narrate popup is dismissed");
+
+    $(NarrateTestUtils.TOGGLE).click();
+    // A focus will follow a real click.
+    $(NarrateTestUtils.TOGGLE).focus();
+    ok(NarrateTestUtils.isVisible($(NarrateTestUtils.POPUP)),
+      "narrate popup is showing again");
+    ok(!NarrateTestUtils.isVisible($(NarrateTestUtils.VOICE_OPTIONS)),
+      "voice options are hidden after popup comes back");
+  });
+});
+
+add_task(function* testVoiceselectLabelChange() {
+  setup();
+
+  yield spawnInNewReaderTab(TEST_ARTICLE, function* () {
+    let $ = content.document.querySelector.bind(content.document);
+
+    $(NarrateTestUtils.TOGGLE).click();
+    ok(NarrateTestUtils.isVisible($(NarrateTestUtils.POPUP)),
+      "popup is toggled");
+
+    ok(NarrateTestUtils.selectVoice(content, "urn:moz-tts:fake-direct:lenny"),
+      "voice selected");
+
+    let selectedOption = $(NarrateTestUtils.VOICE_SELECTED);
+    let selectLabel = $(NarrateTestUtils.VOICE_SELECT_LABEL);
+
+    is(selectedOption.textContent, selectLabel.textContent,
+      "new label matches selected voice");
+  });
+});
+
+add_task(function* testVoiceselectKeyboard() {
+  setup();
+
+  yield spawnInNewReaderTab(TEST_ARTICLE, function* () {
+    let $ = content.document.querySelector.bind(content.document);
+
+    $(NarrateTestUtils.TOGGLE).click();
+    ok(NarrateTestUtils.isVisible($(NarrateTestUtils.POPUP)),
+      "popup is toggled");
+
+    let eventUtils = NarrateTestUtils.getEventUtils(content);
+
+    let firstValue = $(NarrateTestUtils.VOICE_SELECTED).dataset.value;
+
+    ok(!NarrateTestUtils.isVisible($(NarrateTestUtils.VOICE_OPTIONS)),
+      "voice options initially are hidden");
+
+    $(NarrateTestUtils.VOICE_SELECT).focus();
+
+    eventUtils.sendKey("DOWN", content);
+
+    yield ContentTaskUtils.waitForCondition(
+      () => $(NarrateTestUtils.VOICE_SELECTED).dataset.value != firstValue,
+      "value changed after pressing DOWN key");
+
+    eventUtils.sendKey("RETURN", content);
+
+    ok(NarrateTestUtils.isVisible($(NarrateTestUtils.VOICE_OPTIONS)),
+      "voice options showing after pressing RETURN");
+
+    eventUtils.sendKey("UP", content);
+
+    eventUtils.sendKey("RETURN", content);
+
+    ok(!NarrateTestUtils.isVisible($(NarrateTestUtils.VOICE_OPTIONS)),
+      "voice options hidden after pressing RETURN");
+
+    yield ContentTaskUtils.waitForCondition(
+      () => $(NarrateTestUtils.VOICE_SELECTED).dataset.value == firstValue,
+      "value changed back to original after pressing RETURN");
+  });
+});
new file mode 100644
--- /dev/null
+++ b/toolkit/components/narrate/test/head.js
@@ -0,0 +1,67 @@
+/* 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/. */
+
+/* exported teardown, setup, toggleExtension,
+   spawnInNewReaderTab, TEST_ARTICLE  */
+
+"use strict";
+
+const TEST_ARTICLE = "http://example.com/browser/browser/base/content/test/" +
+  "general/readerModeArticle.html";
+
+Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "Promise",
+  "resource://gre/modules/Promise.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Services",
+  "resource://gre/modules/Services.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "AddonManager",
+  "resource://gre/modules/AddonManager.jsm");
+
+const TEST_PREFS = [
+  ["reader.parse-on-load.enabled", true],
+  ["media.webspeech.synth.enabled", true],
+  ["media.webspeech.synth.test", true],
+  ["narrate.enabled", true],
+  ["narrate.test", true]
+];
+
+function setup() {
+  // Set required test prefs.
+  TEST_PREFS.forEach(([name, value]) => {
+    setBoolPref(name, value);
+  });
+}
+
+function teardown() {
+  // Reset test prefs.
+  TEST_PREFS.forEach(pref => {
+    clearUserPref(pref[0]);
+  });
+}
+
+function spawnInNewReaderTab(url, func) {
+  return BrowserTestUtils.withNewTab(
+    { gBrowser,
+      url: `about:reader?url=${encodeURIComponent(url)}` },
+      function* (browser) {
+        yield ContentTask.spawn(browser, null, function* () {
+          Components.utils.import("chrome://mochitests/content/browser/" +
+            "toolkit/components/narrate/test/NarrateTestUtils.jsm");
+
+          yield NarrateTestUtils.getReaderReadyPromise(content);
+        });
+
+        yield ContentTask.spawn(browser, null, func);
+      });
+}
+
+function setBoolPref(name, value) {
+  Services.prefs.setBoolPref(name, value);
+}
+
+function clearUserPref(name) {
+  Services.prefs.clearUserPref(name);
+}
+
--- a/toolkit/components/reader/AboutReader.jsm
+++ b/toolkit/components/reader/AboutReader.jsm
@@ -10,16 +10,17 @@ this.EXPORTED_SYMBOLS = [ "AboutReader" 
 
 Cu.import("resource://gre/modules/ReaderMode.jsm");
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "Rect", "resource://gre/modules/Geometry.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "Task", "resource://gre/modules/Task.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "UITelemetry", "resource://gre/modules/UITelemetry.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "NarrateControls", "resource://gre/modules/narrate/NarrateControls.jsm");
 
 var gStrings = Services.strings.createBundle("chrome://global/locale/aboutReader.properties");
 
 var AboutReader = function(mm, win, articlePromise) {
   let url = this._getOriginalUrl(win);
   if (!(url.startsWith("http://") || url.startsWith("https://"))) {
     let errorMsg = "Only http:// and https:// URLs can be loaded in about:reader.";
     if (Services.prefs.getBoolPref("reader.errors.includeURLs"))
@@ -97,16 +98,20 @@ var AboutReader = function(mm, win, arti
   ];
 
   let fontType = Services.prefs.getCharPref("reader.font_type");
   this._setupSegmentedButton("font-type-buttons", fontTypeOptions, fontType, this._setFontType.bind(this));
   this._setFontType(fontType);
 
   this._setupFontSizeButtons();
 
+  if (win.speechSynthesis && Services.prefs.getBoolPref("narrate.enabled")) {
+    new NarrateControls(mm, win);
+  }
+
   this._loadArticle();
 }
 
 AboutReader.prototype = {
   _BLOCK_IMAGES_SELECTOR: ".content p > img:only-child, " +
                           ".content p > a:only-child > img:only-child, " +
                           ".content .wp-caption img, " +
                           ".content figure img",
@@ -163,17 +168,17 @@ AboutReader.prototype = {
     return _viewId;
   },
 
   receiveMessage: function (message) {
     switch (message.name) {
       // Triggered by Android user pressing BACK while the banner font-dropdown is open.
       case "Reader:CloseDropdown": {
         // Just close it.
-        this._closeDropdown();
+        this._closeDropdowns();
         break;
       }
 
       case "Reader:AddButton": {
         if (message.data.id && message.data.image &&
             !this._doc.getElementById(message.data.id)) {
           let btn = this._doc.createElement("button");
           btn.setAttribute("class", "button");
@@ -204,42 +209,50 @@ AboutReader.prototype = {
 
   handleEvent: function(aEvent) {
     if (!aEvent.isTrusted)
       return;
 
     switch (aEvent.type) {
       case "click":
         let target = aEvent.target;
-        while (target && target.id != "reader-popup")
-          target = target.parentNode;
-        if (!target)
-          this._closeDropdown();
+        if (target.classList.contains('dropdown-toggle')) {
+          this._toggleDropdownClicked(aEvent);
+        } else if (!target.closest('.dropdown-popup')) {
+          this._closeDropdowns();
+        }
         break;
       case "scroll":
-        this._closeDropdown();
+        this._closeDropdowns();
         let isScrollingUp = this._scrollOffset > aEvent.pageY;
         this._setSystemUIVisibility(isScrollingUp);
         this._scrollOffset = aEvent.pageY;
         break;
       case "resize":
         this._updateImageMargins();
+        if (this._isToolbarVertical) {
+          this._win.setTimeout(() => {
+            for (let dropdown of this._doc.querySelectorAll('.dropdown.open')) {
+              this._updatePopupPosition(dropdown);
+            }
+          }, 0);
+        }
         break;
 
       case "devicelight":
         this._handleDeviceLight(aEvent.value);
         break;
 
       case "visibilitychange":
         this._handleVisibilityChange();
         break;
 
       case "unload":
         // Close the Banners Font-dropdown, cleanup Android BackPressListener.
-        this._closeDropdown();
+        this._closeDropdowns();
 
         this._mm.removeMessageListener("Reader:CloseDropdown", this);
         this._mm.removeMessageListener("Reader:AddButton", this);
         this._mm.removeMessageListener("Reader:RemoveButton", this);
         this._windowUnloaded = true;
         break;
     }
   },
@@ -598,17 +611,17 @@ AboutReader.prototype = {
     this._maybeSetTextDirection(article);
 
     this._contentElement.style.display = "block";
     this._updateImageMargins();
 
     this._requestFavicon();
     this._doc.body.classList.add("loaded");
 
-    Services.obs.notifyObservers(null, "AboutReader:Ready", "");
+    Services.obs.notifyObservers(this._win, "AboutReader:Ready", "");
   },
 
   _hideContent: function() {
     this._headerElement.style.display = "none";
     this._contentElement.style.display = "none";
   },
 
   _showProgressDelayed: function() {
@@ -713,80 +726,71 @@ AboutReader.prototype = {
    * @param   Localizable string providing UI element usage tip.
    */
   _setButtonTip: function(id, titleEntity) {
     let button = this._doc.getElementById(id);
     button.setAttribute("title", gStrings.GetStringFromName(titleEntity));
   },
 
   _setupStyleDropdown: function() {
-    let doc = this._doc;
-    let win = this._win;
+    let dropdownToggle = this._doc.querySelector("#style-dropdown .dropdown-toggle");
+    dropdownToggle.setAttribute("title", gStrings.GetStringFromName("aboutReader.toolbar.typeControls"));
+  },
 
-    let dropdown = doc.getElementById("style-dropdown");
+  _updatePopupPosition: function(dropdown) {
     let dropdownToggle = dropdown.querySelector(".dropdown-toggle");
     let dropdownPopup = dropdown.querySelector(".dropdown-popup");
 
-    // Helper function used to position the popup on desktop,
-    // where there is a vertical toolbar.
-    function updatePopupPosition() {
-      let toggleHeight = dropdownToggle.offsetHeight;
-      let toggleTop = dropdownToggle.offsetTop;
-      let popupTop = toggleTop - toggleHeight / 2;
-      dropdownPopup.style.top = popupTop + "px";
-    }
+    let toggleHeight = dropdownToggle.offsetHeight;
+    let toggleTop = dropdownToggle.offsetTop;
+    let popupTop = toggleTop - toggleHeight / 2;
+
+    dropdownPopup.style.top = popupTop + "px";
+  },
+
+  _toggleDropdownClicked: function(event) {
+    let dropdown = event.target.closest('.dropdown');
 
-    if (this._isToolbarVertical) {
-      win.addEventListener("resize", event => {
-        if (!event.isTrusted)
-          return;
+    if (!dropdown)
+      return;
+
+    event.stopPropagation();
 
-        // Wait for reflow before calculating the new position of the popup.
-        win.setTimeout(updatePopupPosition, 0);
-      }, true);
+    if (dropdown.classList.contains("open")) {
+      this._closeDropdowns();
+    } else {
+      this._openDropdown(dropdown);
+      if (this._isToolbarVertical) {
+        this._updatePopupPosition(dropdown);
+      }
     }
-
-    dropdownToggle.setAttribute("title", gStrings.GetStringFromName("aboutReader.toolbar.typeControls"));
-    dropdownToggle.addEventListener("click", event => {
-      if (!event.isTrusted)
-        return;
-
-      event.stopPropagation();
-
-      if (dropdown.classList.contains("open")) {
-        this._closeDropdown();
-      } else {
-        this._openDropdown();
-        if (this._isToolbarVertical) {
-          updatePopupPosition();
-        }
-      }
-    }, true);
   },
 
   /*
    * If the ReaderView banner font-dropdown is closed, open it.
    */
-  _openDropdown: function() {
-    let dropdown = this._doc.getElementById("style-dropdown");
+  _openDropdown: function(dropdown) {
     if (dropdown.classList.contains("open")) {
       return;
     }
 
+    this._closeDropdowns();
+
     // Trigger BackPressListener initialization in Android.
     dropdown.classList.add("open");
     this._mm.sendAsyncMessage("Reader:DropdownOpened", this.viewId);
   },
 
   /*
-   * If the ReaderView banner font-dropdown is opened, close it.
+   * If the ReaderView has open dropdowns, close them.
    */
-  _closeDropdown: function() {
-    let dropdown = this._doc.getElementById("style-dropdown");
-    if (!dropdown.classList.contains("open")) {
-      return;
+  _closeDropdowns: function() {
+    let openDropdowns = this._doc.querySelectorAll(".dropdown.open:not(.keep-open)");
+    for (let dropdown of openDropdowns) {
+      dropdown.classList.remove("open");
     }
 
     // Trigger BackPressListener cleanup in Android.
-    dropdown.classList.remove("open");
-    this._mm.sendAsyncMessage("Reader:DropdownClosed", this.viewId);
-  },
+    if (openDropdowns.length) {
+      this._mm.sendAsyncMessage("Reader:DropdownClosed", this.viewId);
+    }
+  }
 };
new file mode 100644
--- /dev/null
+++ b/toolkit/locales/en-US/chrome/global/narrate.properties
@@ -0,0 +1,19 @@
+# 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/.
+
+# Narrate, meaning "read the page out loud". This is the name of the feature
+# and it is the label for the popup button.
+narrate = Narrate
+back = Back
+start = Start
+stop = Stop
+forward = Forward
+speed = Speed
+selectvoicelabel = Voice:
+# Default voice is determined by the language of the document.
+defaultvoice = Default
+
+# Voice name and language.
+# eg. David (English)
+voiceLabel = %S (%S)
\ No newline at end of file
--- a/toolkit/locales/jar.mn
+++ b/toolkit/locales/jar.mn
@@ -57,16 +57,17 @@
 #endif
   locale/@AB_CD@/global/globalKeys.dtd                  (%chrome/global/globalKeys.dtd)
   locale/@AB_CD@/global/headsUpDisplay.properties       (%chrome/global/headsUpDisplay.properties)
   locale/@AB_CD@/global/intl.css                        (%chrome/global/intl.css)
   locale/@AB_CD@/global/intl.properties                 (%chrome/global/intl.properties)
   locale/@AB_CD@/global/keys.properties                 (%chrome/global/keys.properties)
   locale/@AB_CD@/global/languageNames.properties        (%chrome/global/languageNames.properties)
   locale/@AB_CD@/global/mozilla.dtd                     (%chrome/global/mozilla.dtd)
+  locale/@AB_CD@/global/narrate.properties              (%chrome/global/narrate.properties)
   locale/@AB_CD@/global/notification.dtd                (%chrome/global/notification.dtd)
   locale/@AB_CD@/global/preferences.dtd                 (%chrome/global/preferences.dtd)
   locale/@AB_CD@/global/printdialog.dtd                 (%chrome/global/printdialog.dtd)
   locale/@AB_CD@/global/printjoboptions.dtd             (%chrome/global/printjoboptions.dtd)
   locale/@AB_CD@/global/printPageSetup.dtd              (%chrome/global/printPageSetup.dtd)
   locale/@AB_CD@/global/printPreview.dtd                (%chrome/global/printPreview.dtd)
   locale/@AB_CD@/global/printPreviewProgress.dtd        (%chrome/global/printPreviewProgress.dtd)
   locale/@AB_CD@/global/printdialog.properties          (%chrome/global/printdialog.properties)
--- a/toolkit/modules/addons/WebRequest.jsm
+++ b/toolkit/modules/addons/WebRequest.jsm
@@ -82,16 +82,30 @@ function parseExtra(extra, allowed) {
   for (let al of allowed) {
     if (extra && extra.indexOf(al) != -1) {
       result[al] = true;
     }
   }
   return result;
 }
 
+function mergeStatus(data, channel) {
+  try {
+    data.statusCode = channel.responseStatus;
+    let statusText = channel.responseStatusText;
+    let maj = {};
+    let min = {};
+    channel.QueryInterface(Ci.nsIHttpChannelInternal).getResponseVersion(maj, min);
+    data.statusLine = `HTTP/${maj.value}.${min.value} ${data.statusCode} ${statusText}`;
+  } catch (e) {
+    // NS_ERROR_NOT_AVAILABLE might be thrown.
+    Cu.reportError(e);
+  }
+}
+
 var HttpObserverManager;
 
 var ContentPolicyManager = {
   policyData: new Map(),
   policies: new Map(),
   idMap: new Map(),
   nextId: 0,
 
@@ -399,17 +413,17 @@ HttpObserverManager = {
     let policyType = loadInfo ?
                      loadInfo.externalContentPolicyType :
                      Ci.nsIContentPolicy.TYPE_OTHER;
 
     let requestHeaderNames;
     let responseHeaderNames;
 
     let includeStatus = kind === "headersReceived" ||
-                        kind === "onBeforeRedirect" ||
+                        kind === "onRedirect" ||
                         kind === "onStart" ||
                         kind === "onStop";
 
     for (let [callback, opts] of listeners.entries()) {
       if (!this.shouldRunListener(policyType, channel.URI, opts.filter)) {
         continue;
       }
 
@@ -438,17 +452,17 @@ HttpObserverManager = {
         data.requestHeaders = this.getHeaders(channel, "visitRequestHeaders");
         requestHeaderNames = data.requestHeaders.map(h => h.name);
       }
       if (opts.responseHeaders) {
         data.responseHeaders = this.getHeaders(channel, "visitResponseHeaders");
         responseHeaderNames = data.responseHeaders.map(h => h.name);
       }
       if (includeStatus) {
-        data.statusCode = channel.responseStatus;
+        mergeStatus(data, channel);
       }
 
       let result = null;
       try {
         result = callback(data);
       } catch (e) {
         Cu.reportError(e);
       }
--- a/toolkit/themes/shared/aboutReaderContent.css
+++ b/toolkit/themes/shared/aboutReaderContent.css
@@ -50,17 +50,19 @@ p,
 code,
 pre,
 blockquote,
 ul,
 ol,
 li,
 figure,
 .wp-caption {
-  margin: 0 0 30px 0;
+  margin: -10px -10px 20px -10px;
+  padding: 10px;
+  border-radius: 5px;
 }
 
 p > img:only-child,
 p > a:only-child > img:only-child,
 .wp-caption img,
 figure img {
   display: block;
 }
--- a/toolkit/themes/shared/aboutReaderControls.css
+++ b/toolkit/themes/shared/aboutReaderControls.css
@@ -115,32 +115,36 @@
   padding: 0;
 }
 
 .dropdown li {
   margin: 0;
   padding: 0;
 }
 
-/*======= Font style popup =======*/
+/*======= Popup =======*/
 
 .dropdown-popup {
   min-width: 300px;
   text-align: start;
   position: absolute;
   left: 48px; /* offset to account for toolbar width */
   z-index: 1000;
   background-color: #fbfbfb;
   visibility: hidden;
   border-radius: 4px;
   border: 1px solid #b5b5b5;
   border-bottom-width: 0;
   box-shadow: 0 1px 12px #666;
 }
 
+.keep-open .dropdown-popup {
+  z-index: initial;
+}
+
 .dropdown-popup > hr {
   display: none;
 }
 
 .open > .dropdown-popup {
   visibility: visible;
 }
 
@@ -149,16 +153,18 @@
   top: 30px; /* offset arrow from top of popup */
   left: -16px;
   width: 24px;
   height: 24px;
   background-image: url("chrome://global/skin/reader/RM-Type-Controls-Arrow.svg");
   display: block;
 }
 
+/*======= Font style popup =======*/
+
 #font-type-buttons,
 #font-size-buttons,
 #color-scheme-buttons {
   display: flex;
   flex-direction: row;
 }
 
 #font-type-buttons > button:first-child {
--- a/toolkit/themes/shared/jar.inc.mn
+++ b/toolkit/themes/shared/jar.inc.mn
@@ -21,16 +21,26 @@ toolkit.jar:
   skin/classic/global/aboutSupport.css                     (../../shared/aboutSupport.css)
   skin/classic/global/appPicker.css                        (../../shared/appPicker.css)
   skin/classic/global/config.css                           (../../shared/config.css)
   skin/classic/global/icons/info.svg                       (../../shared/incontent-icons/info.svg)
   skin/classic/global/icons/loading-inverted.png           (../../shared/icons/loading-inverted.png)
   skin/classic/global/icons/loading-inverted@2x.png        (../../shared/icons/loading-inverted@2x.png)
   skin/classic/global/icons/warning.svg                    (../../shared/incontent-icons/warning.svg)
   skin/classic/global/alerts/alert-common.css              (../../shared/alert-common.css)
+  skin/classic/global/narrate.css                          (../../shared/narrate.css)
+  skin/classic/global/narrateControls.css                  (../../shared/narrateControls.css)
+  skin/classic/global/narrate/arrow.svg                     (../../shared/narrate/arrow.svg)
+  skin/classic/global/narrate/back.svg                     (../../shared/narrate/back.svg)
+  skin/classic/global/narrate/fast.svg                     (../../shared/narrate/fast.svg)
+  skin/classic/global/narrate/forward.svg                  (../../shared/narrate/forward.svg)
+  skin/classic/global/narrate/narrate.svg                  (../../shared/narrate/narrate.svg)
+  skin/classic/global/narrate/slow.svg                     (../../shared/narrate/slow.svg)
+  skin/classic/global/narrate/start.svg                    (../../shared/narrate/start.svg)
+  skin/classic/global/narrate/stop.svg                     (../../shared/narrate/stop.svg)
   skin/classic/global/menu/shared-menu-check@2x.png        (../../shared/menu-check@2x.png)
   skin/classic/global/menu/shared-menu-check.png           (../../shared/menu-check.png)
   skin/classic/global/menu/shared-menu-check-active.svg    (../../shared/menu-check-active.svg)
   skin/classic/global/menu/shared-menu-check-black.svg     (../../shared/menu-check-black.svg)
   skin/classic/global/menu/shared-menu-check-hover.svg     (../../shared/menu-check-hover.svg)
   skin/classic/global/in-content/check.svg                 (../../shared/in-content/check.svg)
   skin/classic/global/in-content/check-partial.svg         (../../shared/in-content/check-partial.svg)
   skin/classic/global/in-content/dropdown.svg              (../../shared/in-content/dropdown.svg)
new file mode 100644
--- /dev/null
+++ b/toolkit/themes/shared/narrate.css
@@ -0,0 +1,11 @@
+body.light .narrating {
+  background-color: #ffc;
+}
+
+body.sepia .narrating {
+  background-color: #e0d7c5;
+}
+
+body.dark .narrating {
+  background-color: #242424;
+}
new file mode 100644
--- /dev/null
+++ b/toolkit/themes/shared/narrate/arrow.svg
@@ -0,0 +1,3 @@
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 12 12">
+  <path d="M6 9L1 4l1-1 4 4 4-4 1 1z" fill="#4C4C4C"/>
+</svg>
new file mode 100644
--- /dev/null
+++ b/toolkit/themes/shared/narrate/back.svg
@@ -0,0 +1,15 @@
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 24 24">
+  <defs>
+    <style>
+      use:not(:target) {
+        display: none;
+      }
+      #disabled {
+        opacity: 0.5;
+      }
+    </style>
+    <path id="shape" d="M 5 0 C 4.446 0 4 0.446 4 1 L 4 23 C 4 23.554 4.446 24 5 24 L 7 24 C 7.554 24 8 23.554 8 23 L 8 12.404297 C 8.04108 12.509297 8.109944 12.610125 8.203125 12.703125 L 19.296875 23.775391 C 19.495259 23.972391 19.661613 24.039562 19.796875 23.976562 C 19.932137 23.915564 20 23.748516 20 23.478516 L 20 0.52148438 C 20 0.25248437 19.93214 0.084484365 19.796875 0.021484375 C 19.661613 -0.040515625 19.495259 0.02856248 19.296875 0.2265625 L 8.203125 11.298828 C 8.1099445 11.381828 8.04108 11.481703 8 11.595703 L 8 1 C 8 0.446 7.554 0 7 0 L 5 0 z " fill="gray"/>
+  </defs>
+  <use id="enabled" xlink:href="#shape"/>
+  <use id="disabled" xlink:href="#shape"/>
+</svg>
new file mode 100644
--- /dev/null
+++ b/toolkit/themes/shared/narrate/fast.svg
@@ -0,0 +1,3 @@
+<svg id="Icons" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 20.4">
+    <path fill="gray" d="M14.42 16.68a.77.77 0 0 0 .54.7l2.51.68a1.58 1.58 0 0 1 1.06 1.22l.05.39-3.89-.53a4.34 4.34 0 0 1-1.74-.72L7.2 14.03a5.79 5.79 0 0 1-5.34-4.88h-.82a1 1 0 0 1-1-1l2.9-3.24a6.16 6.16 0 0 1 4.7-2.39 5.88 5.88 0 0 1 .77.05 5 5 0 0 1 .87.15c3.75 1 6.5 5.84 6.5 5.84a2.27 2.27 0 0 0 1.14.85h.17a1.27 1.27 0 0 0 1.22-.4l.78-1-2.47-1.2c-3.38-1.46-2.46-5.71-2.46-5.71 0-.26.23-.32.42-.14l5.32 5-4.31-4.81a1.39 1.39 0 0 1 .81-1.22l4.17 6.65.33.31 2.19 1.54a2.44 2.44 0 0 1 .92 1.75v2.77l-.16.13a1.66 1.66 0 0 1-1.63.19l-.75-.36a2.57 2.57 0 0 0-2.55.32l-2.18 1.82a4.28 4.28 0 0 1-.89.55 10.18 10.18 0 0 0-4.62-8.46c-.27-.16-.66.31-.47.48a10.52 10.52 0 0 1 3.68 8.5v.48zm8.38-5.42a.49.49 0 1 0-.49-.49.49.49 0 0 0 .49.49zm-18 9.14v-.52a1.39 1.39 0 0 1 .93-1.25s2.7-.66 3.43-1.84l2.06 1.63a25.62 25.62 0 0 1-6.43 2z"/>
+</svg>
new file mode 100644
--- /dev/null
+++ b/toolkit/themes/shared/narrate/forward.svg
@@ -0,0 +1,15 @@
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 24 24">
+  <defs>
+    <style>
+      use:not(:target) {
+        display: none;
+      }
+      #disabled {
+        opacity: 0.5;
+      }
+    </style>
+    <path id="shape" d="m 19,0 c 0.554,0 1,0.446 1,1 l 0,22 c 0,0.554 -0.446,1 -1,1 l -2,0 c -0.554,0 -1,-0.446 -1,-1 l 0,-10.595703 c -0.04108,0.105 -0.109944,0.205828 -0.203125,0.298828 L 4.703125,23.775391 c -0.198384,0.197 -0.364738,0.264171 -0.5,0.201171 C 4.067863,23.915564 4,23.748516 4,23.478516 L 4,0.52148438 c 0,-0.26900001 0.06786,-0.43700001 0.203125,-0.5 0.135262,-0.062 0.301616,0.0070781 0.5,0.20507812 l 11.09375,11.0722655 c 0.09318,0.083 0.162045,0.182875 0.203125,0.296875 L 16,1 c 0,-0.554 0.446,-1 1,-1 l 2,0 z" fill="gray"/>
+  </defs>
+  <use id="enabled" xlink:href="#shape"/>
+  <use id="disabled" xlink:href="#shape"/>
+</svg>
new file mode 100644
--- /dev/null
+++ b/toolkit/themes/shared/narrate/narrate.svg
@@ -0,0 +1,3 @@
+<svg id="Icons" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 22 18.77">
+    <path fill="gray" d="M3.13 13.72a1.57 1.57 0 0 1-3.13 0V5.41a1.57 1.57 0 0 1 3.13 0v8.31zm6.29 3.62a1.57 1.57 0 0 1-3.13 0V1.44a1.57 1.57 0 0 1 3.13 0v15.9zm6.29-2.9a1.57 1.57 0 0 1-3.13 0V4.83a1.57 1.57 0 0 1 3.13 0v9.61zM22 12.62a1.57 1.57 0 0 1-3.13 0V6.15a1.57 1.57 0 0 1 3.13 0v6.47z"/>
+</svg>
new file mode 100644
--- /dev/null
+++ b/toolkit/themes/shared/narrate/slow.svg
@@ -0,0 +1,6 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
+    <g fill="gray">
+        <path d="M1.684,13.486c-0.209,0-0.404-0.132-0.474-0.341c-0.528-1.58-0.23-5.767,4.097-7.921 c1.315-0.656,2.589-0.988,3.787-0.988c3.237,0,5.096,2.341,5.99,3.465c0.158,0.199,0.181,0.533,0,0.713 c-0.793,0.794-1.852,1.542-3.231,2.286c-2.46,1.327-5.045,1.775-7.121,2.134c-1.123,0.194-2.093,0.361-2.89,0.627 C1.789,13.479,1.735,13.486,1.684,13.486L1.684,13.486z"/>
+        <path d="M23.185,5.465c-0.86-1.121-2.074-1.819-3.168-1.819c-0.641,0-1.556,0.23-2.273,1.328 c-0.374,0.571-0.577,1.161-0.773,1.73c-0.512,1.482-1.041,3.016-4.662,4.969c-2.316,1.249-4.707,1.664-6.815,2.03 c-2.524,0.438-4.704,0.814-5.455,2.622c-0.069,0.165-0.045,0.354,0.062,0.495c0.107,0.143,0.281,0.217,0.46,0.193 c0.667-0.081,1.533,0.041,2.434,0.217c-0.122,0.146-0.261,0.286-0.391,0.418c-0.38,0.385-0.774,0.783-0.657,1.292 c0.108,0.474,0.604,0.699,0.966,0.828c0.399,0.142,0.843,0.217,1.283,0.217c1.241,0,2.216-0.579,2.649-1.539 c1.704,0.287,3.487,0.313,5.043,0.313l1.639-0.006c0.066,0.056,0.178,0.166,0.264,0.25c0.504,0.506,1.348,1.351,2.721,1.351 c0.129,0,0.264-0.008,0.416-0.026c0.687-0.102,1.351-0.267,1.574-0.787c0.227-0.528-0.123-1.023-0.526-1.597 c-0.481-0.685-1.08-1.532-0.998-2.652c0.196-0.397,0.368-0.824,0.546-1.267c0.479-1.19,0.975-2.421,2.12-3.513 c0.431,0.343,1.022,0.549,1.63,0.549l0,0c0.439,0,0.876-0.102,1.295-0.3c0.624-0.293,1.104-0.967,1.316-1.847 C24.175,7.707,23.914,6.418,23.185,5.465L23.185,5.465z M20.397,7.757c-0.276,0-0.5-0.224-0.5-0.5s0.224-0.5,0.5-0.5 c0.275,0,0.5,0.224,0.5,0.5S20.674,7.757,20.397,7.757z"/>
+    </g>
+</svg>
new file mode 100644
--- /dev/null
+++ b/toolkit/themes/shared/narrate/start.svg
@@ -0,0 +1,3 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
+    <path d="M21.64 12.44L2.827 22.895c-.217.123-.403.137-.56.042-.155-.094-.233-.264-.233-.51V1.572c0-.244.08-.414.233-.51.157-.093.343-.08.56.044L21.642 11.56c.217.124.326.27.326.44 0 .17-.11.316-.327.44z" fill="gray"/>
+</svg>
new file mode 100644
--- /dev/null
+++ b/toolkit/themes/shared/narrate/stop.svg
@@ -0,0 +1,3 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
+    <rect ry="1" rx="1" y="2" x="2" height="20" width="20" fill="gray"/>
+</svg>
new file mode 100644
--- /dev/null
+++ b/toolkit/themes/shared/narrateControls.css
@@ -0,0 +1,184 @@
+:scope {
+  --border-color: #e5e5e5;
+}
+
+#narrate-toggle {
+  background-image: url("chrome://global/skin/narrate/narrate.svg");
+}
+
+.dropdown-popup button {
+  background-color: transparent;
+}
+
+.dropdown-popup button:hover:not(:disabled) {
+  background-color: #eaeaea;
+}
+
+.narrate-row {
+  display: flex;
+  align-items: center;
+  min-height: 40px;
+  box-sizing: border-box;
+}
+
+.narrate-row:not(:first-child) {
+  border-top: 1px solid var(--border-color);
+}
+
+/* Control buttons */
+
+#narrate-control > button {
+  background-size: 24px 24px;
+  background-repeat: no-repeat;
+  background-position: center center;
+  height: 64px;
+  width: 100px;
+  border: none;
+  color: #666;
+  box-sizing: border-box;
+}
+
+#narrate-control > button:not(:first-child) {
+  border-left: 1px solid var(--border-color);
+}
+
+#narrate-skip-previous {
+  border-top-left-radius: 3px;
+  background-image: url("chrome://global/skin/narrate/back.svg#enabled");
+}
+
+#narrate-skip-next {
+  border-top-right-radius: 3px;
+  background-image: url("chrome://global/skin/narrate/forward.svg#enabled");
+}
+
+#narrate-skip-previous:disabled {
+  background-image: url("chrome://global/skin/narrate/back.svg#disabled");
+}
+
+#narrate-skip-next:disabled {
+  background-image: url("chrome://global/skin/narrate/forward.svg#disabled");
+}
+
+#narrate-start-stop {
+  background-image: url("chrome://global/skin/narrate/start.svg");
+}
+
+#narrate-start-stop.speaking {
+  background-image: url("chrome://global/skin/narrate/stop.svg");
+}
+
+/* Rate control */
+
+#narrate-rate::before, #narrate-rate::after {
+  content: '';
+  width: 48px;
+  height: 40px;
+  background-position: center;
+  background-repeat: no-repeat;
+  background-size: 24px auto;
+}
+
+#narrate-rate::before {
+  background-image: url("chrome://global/skin/narrate/slow.svg");
+}
+
+#narrate-rate::after {
+  background-image: url("chrome://global/skin/narrate/fast.svg");
+}
+
+#narrate-rate-input {
+  margin: 0 1px;
+  flex-grow: 1;
+}
+
+#narrate-rate-input::-moz-range-track {
+  background-color: #979797;
+  height: 2px;
+}
+
+#narrate-rate-input::-moz-range-progress {
+  background-color: #2EA3FF;
+  height: 2px;
+}
+
+#narrate-rate-input::-moz-range-thumb {
+  background-color: #808080;
+  height: 16px;
+  width: 16px;
+  border-radius: 8px;
+  border-width: 0;
+}
+
+#narrate-rate-input:active::-moz-range-thumb {
+  background-color: #2EA3FF;
+}
+
+/* Voice selection */
+
+.voiceselect {
+  width: 100%;
+}
+
+.voiceselect > button.select-toggle,
+.voiceselect > .options > button.option {
+  -moz-appearance: none;
+  border: none;
+  width: 100%;
+  min-height: 40px;
+}
+
+.voiceselect.open > button.select-toggle {
+  border-bottom: 1px solid var(--border-color);
+}
+
+.voiceselect > button.select-toggle::after {
+  content: '';
+  background-image: url("chrome://global/skin/narrate/arrow.svg");
+  background-position: center;
+  background-repeat: no-repeat;
+  background-size: 12px 12px;
+  display: inline-block;
+  width: 1.5em;
+  height: 1em;
+  vertical-align: middle;
+}
+
+.voiceselect > .options > button.option:not(:first-child) {
+  border-top: 1px solid var(--border-color);
+}
+
+.voiceselect > .options > button.option  {
+  box-sizing: border-box;
+}
+
+.voiceselect > .options:not(.hovering) > button.option:focus {
+  background-color: #eaeaea;
+}
+
+.voiceselect > .options:not(.hovering) > button.option:hover:not(:focus) {
+  background-color: transparent;
+}
+
+.voiceselect > .options > button.option::-moz-focus-inner {
+  outline: none;
+  border: 0;
+}
+
+.voiceselect > .options {
+  display: none;
+  overflow-y: auto;
+}
+
+.voiceselect.open > .options {
+  display: block;
+}
+
+.current-voice {
+  color: #7f7f7f;
+}
+
+.voiceselect:not(.open) > button,
+.option:last-child {
+  border-radius: 0 0 3px 3px;
+}
--- a/xpcom/base/nsIMemory.idl
+++ b/xpcom/base/nsIMemory.idl
@@ -63,25 +63,16 @@ interface nsIMemory : nsISupports
      *   false, the flush will be scheduled to happen when the app is
      *   idle.
      * @throws NS_ERROR_FAILURE if 'immediate' is set an the call
      *   was not on the application's main thread.
      */
     void heapMinimize(in boolean immediate);
 
     /**
-     * This predicate can be used to determine if we're in a low-memory
-     * situation (what constitutes low-memory is platform dependent). This
-     * can be used to trigger the memory pressure observers.
-     *
-     * DEPRECATED - Always returns false.  See bug 592308.
-     */
-    boolean isLowMemory();
-
-    /**
      * This predicate can be used to determine if the platform is a "low-memory"
      * platform. Callers may use this to dynamically tune their behaviour
      * to favour reduced memory usage at the expense of performance. The value
      * returned by this function will not change over the lifetime of the process.
      */
     boolean isLowMemoryPlatform();
 };
 
--- a/xpcom/base/nsMemoryImpl.cpp
+++ b/xpcom/base/nsMemoryImpl.cpp
@@ -30,24 +30,16 @@ NS_IMPL_QUERY_INTERFACE(nsMemoryImpl, ns
 
 NS_IMETHODIMP
 nsMemoryImpl::HeapMinimize(bool aImmediate)
 {
   return FlushMemory(MOZ_UTF16("heap-minimize"), aImmediate);
 }
 
 NS_IMETHODIMP
-nsMemoryImpl::IsLowMemory(bool* aResult)
-{
-  NS_ERROR("IsLowMemory is deprecated.  See bug 592308.");
-  *aResult = false;
-  return NS_OK;
-}
-
-NS_IMETHODIMP
 nsMemoryImpl::IsLowMemoryPlatform(bool* aResult)
 {
 #ifdef ANDROID
   static int sLowMemory = -1; // initialize to unknown, lazily evaluate to 0 or 1
   if (sLowMemory == -1) {
     sLowMemory = 0; // assume "not low memory" in case file operations fail
     *aResult = false;