Merge autoland to mozilla-central. a=merge
authorCosmin Sabou <csabou@mozilla.com>
Sat, 19 Jan 2019 11:27:44 +0200
changeset 514499 64d167665c2906410aaab43c3346edde9a3428df
parent 514480 a71f2830d57099dc5e83af1cfbcf98c97c68f2f7 (current diff)
parent 514498 1c7ed5d9e1fe429f8a753b40156744b73ef12cda (diff)
child 514582 f90bab5af97efa714181eea7fad45cf8cf14e3ea
push id1953
push userffxbld-merge
push dateMon, 11 Mar 2019 12:10:20 +0000
treeherdermozilla-release@9c35dcbaa899 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmerge
milestone66.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Merge autoland to mozilla-central. a=merge
--- a/browser/components/urlbar/UrlbarInput.jsm
+++ b/browser/components/urlbar/UrlbarInput.jsm
@@ -52,16 +52,17 @@ class UrlbarInput {
       browserWindow: this.window,
     });
     this.controller.setInput(this);
     this.view = new UrlbarView(this);
     this.valueIsTyped = false;
     this.userInitiatedFocus = false;
     this.isPrivate = PrivateBrowsingUtils.isWindowPrivate(this.window);
     this._untrimmedValue = "";
+    this._suppressStartQuery = false;
 
     // Forward textbox methods and properties.
     const METHODS = ["addEventListener", "removeEventListener",
       "setAttribute", "hasAttribute", "removeAttribute", "getAttribute",
       "focus", "blur", "select"];
     const READ_ONLY_PROPERTIES = ["inputField", "editor"];
     const READ_WRITE_PROPERTIES = ["placeholder", "readOnly",
       "selectionStart", "selectionEnd"];
@@ -106,16 +107,17 @@ class UrlbarInput {
     this.inputField.addEventListener("underflow", this);
     this.inputField.addEventListener("scrollend", this);
     this.inputField.addEventListener("select", this);
     this.inputField.addEventListener("keydown", this);
     this.view.panel.addEventListener("popupshowing", this);
     this.view.panel.addEventListener("popuphidden", this);
 
     this.inputField.controllers.insertControllerAt(0, new CopyCutController(this));
+    this._initPasteAndGo();
   }
 
   /**
    * Shortens the given value, usually by removing http:// and trailing slashes,
    * such that calling nsIURIFixup::createFixupURI with the result will produce
    * the same URI.
    *
    * @param {string} val
@@ -370,16 +372,20 @@ class UrlbarInput {
    * Starts a query based on the user input.
    *
    * @param {number} [options.lastKey]
    *   The last key the user entered (as a key code).
    */
   startQuery({
     lastKey = null,
   } = {}) {
+    if (this._suppressStartQuery) {
+      return;
+    }
+
     this.controller.startQuery(new QueryContext({
       enableAutofill: UrlbarPrefs.get("autoFill"),
       isPrivate: this.isPrivate,
       lastKey,
       maxResults: UrlbarPrefs.get("maxRichResults"),
       muxer: "UnifiedComplete",
       providers: ["UnifiedComplete"],
       searchString: this.textValue,
@@ -691,16 +697,61 @@ class UrlbarInput {
     if (where == "tab" &&
         reuseEmpty &&
         this.window.gBrowser.selectedTab.isEmpty) {
       where = "current";
     }
     return where;
   }
 
+  _initPasteAndGo() {
+    let inputBox = this.document.getAnonymousElementByAttribute(
+                     this.textbox, "anonid", "moz-input-box");
+    // Force the Custom Element to upgrade until Bug 1470242 handles this:
+    this.window.customElements.upgrade(inputBox);
+    let contextMenu = inputBox.menupopup;
+    let insertLocation = contextMenu.firstElementChild;
+    while (insertLocation.nextElementSibling &&
+           insertLocation.getAttribute("cmd") != "cmd_paste") {
+      insertLocation = insertLocation.nextElementSibling;
+    }
+    if (!insertLocation) {
+      return;
+    }
+
+    let pasteAndGo = this.document.createXULElement("menuitem");
+    let label = Services.strings
+                        .createBundle("chrome://browser/locale/browser.properties")
+                        .GetStringFromName("pasteAndGo.label");
+    pasteAndGo.setAttribute("label", label);
+    pasteAndGo.setAttribute("anonid", "paste-and-go");
+    pasteAndGo.addEventListener("command", () => {
+      this._suppressStartQuery = true;
+
+      this.select();
+      this.window.goDoCommand("cmd_paste");
+      this.handleCommand();
+
+      this._suppressStartQuery = false;
+    });
+
+    contextMenu.addEventListener("popupshowing", () => {
+      let controller =
+        this.document.commandDispatcher.getControllerForCommand("cmd_paste");
+      let enabled = controller.isCommandEnabled("cmd_paste");
+      if (enabled) {
+        pasteAndGo.removeAttribute("disabled");
+      } else {
+        pasteAndGo.setAttribute("disabled", "true");
+      }
+    });
+
+    insertLocation.insertAdjacentElement("afterend", pasteAndGo);
+  }
+
   // Event handlers below.
 
   _on_blur(event) {
     this.formatValue();
   }
 
   _on_focus(event) {
     this._updateUrlTooltip();
--- a/dom/base/nsContentUtils.h
+++ b/dom/base/nsContentUtils.h
@@ -3431,17 +3431,16 @@ class nsContentUtils {
   static uint32_t sHandlingInputTimeout;
   static bool sIsPerformanceTimingEnabled;
   static bool sIsResourceTimingEnabled;
   static bool sIsPerformanceNavigationTimingEnabled;
   static bool sIsUpgradableDisplayContentPrefEnabled;
   static bool sIsFrameTimingPrefEnabled;
   static bool sIsFormAutofillAutocompleteEnabled;
   static bool sIsUAWidgetEnabled;
-  static bool sIsCustomElementsEnabled;
   static bool sSendPerformanceTimingNotifications;
   static bool sUseActivityCursor;
   static bool sAnimationsAPICoreEnabled;
   static bool sGetBoxQuadsEnabled;
   static bool sSkipCursorMoveForSameValueSet;
   static bool sRequestIdleCallbackEnabled;
   static bool sTailingEnabled;
   static bool sShowInputPlaceholderOnFocus;
--- a/dom/base/nsGlobalWindowInner.cpp
+++ b/dom/base/nsGlobalWindowInner.cpp
@@ -5396,18 +5396,17 @@ void nsGlobalWindowInner::Suspend() {
   }
 
   SuspendIdleRequests();
 
   mTimeoutManager->Suspend();
 
   // Suspend all of the AudioContexts for this window
   for (uint32_t i = 0; i < mAudioContexts.Length(); ++i) {
-    ErrorResult dummy;
-    RefPtr<Promise> d = mAudioContexts[i]->Suspend(dummy);
+    mAudioContexts[i]->SuspendFromChrome();
   }
 }
 
 void nsGlobalWindowInner::Resume() {
   MOZ_ASSERT(NS_IsMainThread());
 
   // We can only safely resume a window if its the current inner window.  If
   // its not the current inner, then we are in one of two different cases.
@@ -5438,18 +5437,17 @@ void nsGlobalWindowInner::Resume() {
     for (uint32_t i = 0; i < mEnabledSensors.Length(); i++)
       ac->AddWindowListener(mEnabledSensors[i], this);
   }
   EnableGamepadUpdates();
   EnableVRUpdates();
 
   // Resume all of the AudioContexts for this window
   for (uint32_t i = 0; i < mAudioContexts.Length(); ++i) {
-    ErrorResult dummy;
-    RefPtr<Promise> d = mAudioContexts[i]->Resume(dummy);
+    mAudioContexts[i]->ResumeFromChrome();
   }
 
   mTimeoutManager->Resume();
 
   ResumeIdleRequests();
 
   // Resume all of the workers for this window.  We must do this
   // after timeouts since workers may have queued events that can trigger
--- a/dom/media/test/mochitest.ini
+++ b/dom/media/test/mochitest.ini
@@ -719,16 +719,18 @@ skip-if = android_version >= '23' # bug 
 [test_autoplay_policy_key_blacklist.html]
 skip-if = android_version >= '23' || (verify && debug && (os == 'win')) # bug 1424903
 [test_autoplay_policy_unmute_pauses.html]
 skip-if = android_version >= '23' # bug 1424903
 [test_autoplay_policy_play_before_loadedmetadata.html]
 skip-if = android_version >= '23' # bug 1424903
 [test_autoplay_policy_permission.html]
 skip-if = android_version >= '23' # bug 1424903
+[test_autoplay_policy_web_audio_notResumePageInvokedSuspendedAudioContext.html]
+skip-if = android_version >= '23' # bug 1424903
 [test_autoplay_policy_web_audio_mediaElementAudioSourceNode.html]
 skip-if = android_version >= '23' # bug 1424903
 [test_buffered.html]
 skip-if = android_version == '22' # bug 1308388, android(bug 1232305)
 [test_bug448534.html]
 [test_bug463162.xhtml]
 [test_bug465498.html]
 skip-if = toolkit == 'android' # android(bug 1232305)
new file mode 100644
--- /dev/null
+++ b/dom/media/test/test_autoplay_policy_web_audio_notResumePageInvokedSuspendedAudioContext.html
@@ -0,0 +1,95 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+  <title>Autoplay policy test : do not resume AudioContext which is suspended by page</title>
+  <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+  <script type="text/javascript" src="manifest.js"></script>
+</head>
+<body>
+<script>
+
+/**
+ * This test is used to ensure we won't resume AudioContext which is suspended
+ * by page (it means calling suspend() explicitly) when calling
+ * `AudioScheduledSourceNode.start()`.
+ */
+
+SimpleTest.waitForExplicitFinish();
+
+(async function testNotResumeUserInvokedSuspendedAudioContext() {
+  await setupTestPreferences();
+
+  const nodeTypes = ["AudioBufferSourceNode", "ConstantSourceNode", "OscillatorNode"];
+  for (let nodeType of nodeTypes) {
+    info(`- create an audio context which should not be allowed to start, it's allowed to be created, but it's forbidden to start -`);
+    await createAudioContext();
+
+    info(`- explicitly suspend the AudioContext in the page -`);
+    suspendAudioContext();
+
+    info(`- start an 'AudioScheduledSourceNode', and check that the AudioContext does not start, because it has been explicitly suspended -`);
+    await createAndStartAudioScheduledSourceNode(nodeType);
+  }
+
+  SimpleTest.finish();
+})();
+
+/**
+ * Test utility functions
+ */
+
+function setupTestPreferences() {
+  return SpecialPowers.pushPrefEnv({"set": [
+    ["media.autoplay.default", SpecialPowers.Ci.nsIAutoplay.BLOCKED],
+    ["media.autoplay.enabled.user-gestures-needed", true],
+    ["media.autoplay.block-webaudio", true],
+    ["media.autoplay.block-event.enabled", true],
+  ]});
+}
+
+async function createAudioContext() {
+  window.ac = new AudioContext();
+  await once(ac, "blocked");
+  is(ac.state, "suspended", `AudioContext is blocked.`);
+}
+
+function suspendAudioContext() {
+  try {
+    ac.suspend();
+  } catch(e) {
+    ok(false, `AudioContext suspend failed!`);
+  }
+}
+
+async function createAndStartAudioScheduledSourceNode(nodeType) {
+  let node;
+  info(`- create ${nodeType} -`);
+  switch (nodeType) {
+    case "AudioBufferSourceNode":
+      node = ac.createBufferSource();
+      break;
+    case "ConstantSourceNode":
+      node = ac.createConstantSource();
+      break;
+    case "OscillatorNode":
+      node = ac.createOscillator();
+      break;
+    default:
+      ok(false, "undefined AudioScheduledSourceNode type");
+      return;
+  }
+  node.connect(ac.destination);
+
+  // activate the document in order to allow autoplay.
+  SpecialPowers.wrap(document).notifyUserGestureActivation();
+  node.start();
+
+  await once(ac, "blocked");
+  is(ac.state, "suspended", `AudioContext should not be resumed.`);
+
+  // reset the activation flag of the document in order not to interfere next test.
+  SpecialPowers.wrap(document).clearUserGestureActivation();
+}
+
+</script>
--- a/dom/media/webaudio/AudioContext.cpp
+++ b/dom/media/webaudio/AudioContext.cpp
@@ -148,16 +148,17 @@ AudioContext::AudioContext(nsPIDOMWindow
       mNumberOfChannels(aNumberOfChannels),
       mIsOffline(aIsOffline),
       mIsStarted(!aIsOffline),
       mIsShutDown(false),
       mCloseCalled(false),
       mSuspendCalled(false),
       mIsDisconnecting(false),
       mWasAllowedToStart(true),
+      mSuspendedByContent(false),
       mWasEverAllowedToStart(false),
       mWasEverBlockedToStart(false),
       mWouldBeAllowedToStart(true) {
   bool mute = aWindow->AddAudioContext(this);
 
   // Note: AudioDestinationNode needs an AudioContext that must already be
   // bound to the window.
   const bool allowedToStart = AutoplayPolicy::IsAllowedToPlay(*this);
@@ -188,17 +189,21 @@ void AudioContext::StartBlockedAudioCont
   // Only try to start AudioContext when AudioContext was not allowed to start.
   if (mWasAllowedToStart) {
     return;
   }
 
   const bool isAllowedToPlay = AutoplayPolicy::IsAllowedToPlay(*this);
   AUTOPLAY_LOG("Trying to start AudioContext %p, IsAllowedToPlay=%d", this,
                isAllowedToPlay);
-  if (isAllowedToPlay) {
+
+  // Only start the AudioContext if this resume() call was initiated by content,
+  // not if it was a result of the AudioContext starting after having been
+  // blocked because of the auto-play policy.
+  if (isAllowedToPlay && !mSuspendedByContent) {
     ResumeInternal();
   } else {
     ReportBlocked();
   }
 }
 
 nsresult AudioContext::Init() {
   if (!mIsOffline) {
@@ -897,21 +902,31 @@ already_AddRefed<Promise> AudioContext::
     return promise.forget();
   }
 
   if (mAudioContextState == AudioContextState::Closed || mCloseCalled) {
     promise->MaybeReject(NS_ERROR_DOM_INVALID_STATE_ERR);
     return promise.forget();
   }
 
+  mSuspendedByContent = true;
   mPromiseGripArray.AppendElement(promise);
   SuspendInternal(promise);
   return promise.forget();
 }
 
+void AudioContext::SuspendFromChrome() {
+  // Not support suspend call for these situations.
+  if (mAudioContextState == AudioContextState::Suspended || mIsOffline ||
+      (mAudioContextState == AudioContextState::Closed || mCloseCalled)) {
+    return;
+  }
+  SuspendInternal(nullptr);
+}
+
 void AudioContext::SuspendInternal(void* aPromise) {
   Destination()->Suspend();
 
   nsTArray<MediaStream*> streams;
   // If mSuspendCalled is true then we already suspended all our streams,
   // so don't suspend them again (since suspend(); suspend(); resume(); should
   // cancel both suspends). But we still need to do ApplyAudioContextOperation
   // to ensure our new promise is resolved.
@@ -919,16 +934,25 @@ void AudioContext::SuspendInternal(void*
     streams = GetAllStreams();
   }
   Graph()->ApplyAudioContextOperation(DestinationStream(), streams,
                                       AudioContextOperation::Suspend, aPromise);
 
   mSuspendCalled = true;
 }
 
+void AudioContext::ResumeFromChrome() {
+  // Not support resume call for these situations.
+  if (mAudioContextState == AudioContextState::Running || mIsOffline ||
+      (mAudioContextState == AudioContextState::Closed || mCloseCalled)) {
+    return;
+  }
+  ResumeInternal();
+}
+
 already_AddRefed<Promise> AudioContext::Resume(ErrorResult& aRv) {
   nsCOMPtr<nsIGlobalObject> parentObject = do_QueryInterface(GetParentObject());
   RefPtr<Promise> promise;
   promise = Promise::Create(parentObject, aRv);
   if (aRv.Failed()) {
     return nullptr;
   }
 
@@ -937,16 +961,17 @@ already_AddRefed<Promise> AudioContext::
     return promise.forget();
   }
 
   if (mAudioContextState == AudioContextState::Closed || mCloseCalled) {
     promise->MaybeReject(NS_ERROR_DOM_INVALID_STATE_ERR);
     return promise.forget();
   }
 
+  mSuspendedByContent = false;
   mPendingResumePromises.AppendElement(promise);
 
   const bool isAllowedToPlay = AutoplayPolicy::IsAllowedToPlay(*this);
   AUTOPLAY_LOG("Trying to resume AudioContext %p, IsAllowedToPlay=%d", this,
                isAllowedToPlay);
   if (isAllowedToPlay) {
     ResumeInternal();
   } else {
--- a/dom/media/webaudio/AudioContext.h
+++ b/dom/media/webaudio/AudioContext.h
@@ -196,16 +196,22 @@ class AudioContext final : public DOMEve
   // When back on the main thread, we can resolve or reject the promise, by
   // casting it back to a `Promise*` while asserting we're back on the main
   // thread and removing the reference we added.
   already_AddRefed<Promise> Suspend(ErrorResult& aRv);
   already_AddRefed<Promise> Resume(ErrorResult& aRv);
   already_AddRefed<Promise> Close(ErrorResult& aRv);
   IMPL_EVENT_HANDLER(statechange)
 
+  // These two functions are similar with Suspend() and Resume(), the difference
+  // is they are designed for calling from chrome side, not content side. eg.
+  // calling from inner window, so we won't need to return promise for caller.
+  void SuspendFromChrome();
+  void ResumeFromChrome();
+
   already_AddRefed<AudioBufferSourceNode> CreateBufferSource(ErrorResult& aRv);
 
   already_AddRefed<ConstantSourceNode> CreateConstantSource(ErrorResult& aRv);
 
   already_AddRefed<AudioBuffer> CreateBuffer(uint32_t aNumberOfChannels,
                                              uint32_t aLength,
                                              float aSampleRate,
                                              ErrorResult& aRv);
@@ -375,16 +381,19 @@ class AudioContext final : public DOMEve
   // Close has been called, reject suspend and resume call.
   bool mCloseCalled;
   // Suspend has been called with no following resume.
   bool mSuspendCalled;
   bool mIsDisconnecting;
   // This flag stores the value of previous status of `allowed-to-start`.
   bool mWasAllowedToStart;
 
+  // True if this AudioContext has been suspended by the page.
+  bool mSuspendedByContent;
+
   // These variables are used for telemetry, they're not reflect the actual
   // status of AudioContext, they are based on the "assumption" of enabling
   // blocking web audio. Because we want to record Telemetry no matter user
   // enable blocking autoplay or not.
   // - 'mWasEverAllowedToStart' would be true when AudioContext had ever been
   //   allowed to start if we enable blocking web audio.
   // - 'mWasEverBlockedToStart' would be true when AudioContext had ever been
   //   blocked to start if we enable blocking web audio.
--- a/dom/security/nsCSPContext.cpp
+++ b/dom/security/nsCSPContext.cpp
@@ -39,16 +39,17 @@
 #include "nsThreadUtils.h"
 #include "nsString.h"
 #include "nsScriptSecurityManager.h"
 #include "nsStringStream.h"
 #include "mozilla/Logging.h"
 #include "mozilla/Preferences.h"
 #include "mozilla/dom/CSPReportBinding.h"
 #include "mozilla/dom/CSPDictionariesBinding.h"
+#include "mozilla/ipc/PBackgroundSharedTypes.h"
 #include "mozilla/net/ReferrerPolicy.h"
 #include "nsINetworkInterceptController.h"
 #include "nsSandboxFlags.h"
 #include "nsIScriptElement.h"
 #include "nsIEventTarget.h"
 #include "mozilla/dom/DocGroup.h"
 #include "mozilla/dom/Element.h"
 #include "nsXULAppAPI.h"
@@ -269,71 +270,95 @@ nsresult nsCSPContext::InitFromOther(nsC
   NS_ENSURE_SUCCESS(rv, rv);
 
   for (auto policy : aOtherContext->mPolicies) {
     nsAutoString policyStr;
     policy->toString(policyStr);
     AppendPolicy(policyStr, policy->getReportOnlyFlag(),
                  policy->getDeliveredViaMetaTagFlag());
   }
+  mIPCPolicies = aOtherContext->mIPCPolicies;
   return NS_OK;
 }
 
+void nsCSPContext::SetIPCPolicies(
+    const nsTArray<mozilla::ipc::ContentSecurityPolicy>& aPolicies) {
+  mIPCPolicies = aPolicies;
+}
+
+void nsCSPContext::EnsureIPCPoliciesRead() {
+  if (mIPCPolicies.Length() > 0) {
+    nsresult rv;
+    for (auto& policy : mIPCPolicies) {
+      rv = AppendPolicy(policy.policy(), policy.reportOnlyFlag(),
+                        policy.deliveredViaMetaTagFlag());
+      Unused << NS_WARN_IF(NS_FAILED(rv));
+    }
+    mIPCPolicies.Clear();
+  }
+}
+
 NS_IMETHODIMP
 nsCSPContext::GetPolicyString(uint32_t aIndex, nsAString& outStr) {
   outStr.Truncate();
+  EnsureIPCPoliciesRead();
   if (aIndex >= mPolicies.Length()) {
     return NS_ERROR_ILLEGAL_VALUE;
   }
   mPolicies[aIndex]->toString(outStr);
   return NS_OK;
 }
 
 const nsCSPPolicy* nsCSPContext::GetPolicy(uint32_t aIndex) {
+  EnsureIPCPoliciesRead();
   if (aIndex >= mPolicies.Length()) {
     return nullptr;
   }
   return mPolicies[aIndex];
 }
 
 NS_IMETHODIMP
 nsCSPContext::GetPolicyCount(uint32_t* outPolicyCount) {
+  EnsureIPCPoliciesRead();
   *outPolicyCount = mPolicies.Length();
   return NS_OK;
 }
 
 NS_IMETHODIMP
 nsCSPContext::GetUpgradeInsecureRequests(bool* outUpgradeRequest) {
+  EnsureIPCPoliciesRead();
   *outUpgradeRequest = false;
   for (uint32_t i = 0; i < mPolicies.Length(); i++) {
     if (mPolicies[i]->hasDirective(
             nsIContentSecurityPolicy::UPGRADE_IF_INSECURE_DIRECTIVE)) {
       *outUpgradeRequest = true;
       return NS_OK;
     }
   }
   return NS_OK;
 }
 
 NS_IMETHODIMP
 nsCSPContext::GetBlockAllMixedContent(bool* outBlockAllMixedContent) {
+  EnsureIPCPoliciesRead();
   *outBlockAllMixedContent = false;
   for (uint32_t i = 0; i < mPolicies.Length(); i++) {
     if (!mPolicies[i]->getReportOnlyFlag() &&
         mPolicies[i]->hasDirective(
             nsIContentSecurityPolicy::BLOCK_ALL_MIXED_CONTENT)) {
       *outBlockAllMixedContent = true;
       return NS_OK;
     }
   }
   return NS_OK;
 }
 
 NS_IMETHODIMP
 nsCSPContext::GetEnforcesFrameAncestors(bool* outEnforcesFrameAncestors) {
+  EnsureIPCPoliciesRead();
   *outEnforcesFrameAncestors = false;
   for (uint32_t i = 0; i < mPolicies.Length(); i++) {
     if (!mPolicies[i]->getReportOnlyFlag() &&
         mPolicies[i]->hasDirective(
             nsIContentSecurityPolicy::FRAME_ANCESTORS_DIRECTIVE)) {
       *outEnforcesFrameAncestors = true;
       return NS_OK;
     }
@@ -368,16 +393,17 @@ nsCSPContext::AppendPolicy(const nsAStri
     mPolicies.AppendElement(policy);
   }
   return NS_OK;
 }
 
 NS_IMETHODIMP
 nsCSPContext::GetAllowsEval(bool* outShouldReportViolation,
                             bool* outAllowsEval) {
+  EnsureIPCPoliciesRead();
   *outShouldReportViolation = false;
   *outAllowsEval = true;
 
   for (uint32_t i = 0; i < mPolicies.Length(); i++) {
     if (!mPolicies[i]->allows(nsIContentPolicy::TYPE_SCRIPT, CSP_UNSAFE_EVAL,
                               EmptyString(), false)) {
       // policy is violated: must report the violation and allow the inline
       // script if the policy is report-only.
@@ -448,16 +474,17 @@ nsCSPContext::GetAllowsInline(nsContentP
       "We should only see external content policy types here.");
 
   if (aContentType != nsIContentPolicy::TYPE_SCRIPT &&
       aContentType != nsIContentPolicy::TYPE_STYLESHEET) {
     MOZ_ASSERT(false, "can only allow inline for script or style");
     return NS_OK;
   }
 
+  EnsureIPCPoliciesRead();
   nsAutoString content(EmptyString());
 
   // always iterate all policies, otherwise we might not send out all reports
   for (uint32_t i = 0; i < mPolicies.Length(); i++) {
     bool allowed =
         mPolicies[i]->allows(aContentType, CSP_UNSAFE_INLINE, EmptyString(),
                              aParserCreated) ||
         mPolicies[i]->allows(aContentType, CSP_NONCE, aNonce, aParserCreated);
@@ -580,16 +607,17 @@ nsCSPContext::GetAllowsInline(nsContentP
  *     reports.
  */
 NS_IMETHODIMP
 nsCSPContext::LogViolationDetails(
     uint16_t aViolationType, Element* aTriggeringElement,
     nsICSPEventListener* aCSPEventListener, const nsAString& aSourceFile,
     const nsAString& aScriptSample, int32_t aLineNum, int32_t aColumnNum,
     const nsAString& aNonce, const nsAString& aContent) {
+  EnsureIPCPoliciesRead();
   for (uint32_t p = 0; p < mPolicies.Length(); p++) {
     NS_ASSERTION(mPolicies[p], "null pointer in nsTArray<nsCSPPolicy>");
 
     BlockedContentSource blockedContentSource = BlockedContentSource::eUnknown;
     if (aViolationType == nsIContentSecurityPolicy::VIOLATION_TYPE_EVAL) {
       blockedContentSource = BlockedContentSource::eEval;
     } else if (aViolationType ==
                    nsIContentSecurityPolicy::VIOLATION_TYPE_INLINE_SCRIPT ||
@@ -794,16 +822,17 @@ void StripURIForReporting(nsIURI* aURI, 
 }
 
 nsresult nsCSPContext::GatherSecurityPolicyViolationEventData(
     nsIURI* aBlockedURI, const nsACString& aBlockedString, nsIURI* aOriginalURI,
     nsAString& aViolatedDirective, uint32_t aViolatedPolicyIndex,
     nsAString& aSourceFile, nsAString& aScriptSample, uint32_t aLineNum,
     uint32_t aColumnNum,
     mozilla::dom::SecurityPolicyViolationEventInit& aViolationEventInit) {
+  EnsureIPCPoliciesRead();
   NS_ENSURE_ARG_MAX(aViolatedPolicyIndex, mPolicies.Length() - 1);
 
   MOZ_ASSERT(ValidateDirectiveName(aViolatedDirective),
              "Invalid directive name");
 
   nsresult rv;
 
   // document-uri
@@ -899,16 +928,17 @@ nsresult nsCSPContext::GatherSecurityPol
   aViolationEventInit.mComposed = true;
 
   return NS_OK;
 }
 
 nsresult nsCSPContext::SendReports(
     const mozilla::dom::SecurityPolicyViolationEventInit& aViolationEventInit,
     uint32_t aViolatedPolicyIndex) {
+  EnsureIPCPoliciesRead();
   NS_ENSURE_ARG_MAX(aViolatedPolicyIndex, mPolicies.Length() - 1);
 
   dom::CSPReport report;
 
   // blocked-uri
   report.mCsp_report.mBlocked_uri = aViolationEventInit.mBlockedURI;
 
   // document-uri
@@ -1312,16 +1342,17 @@ class CSPReportSenderRunnable final : pu
  */
 nsresult nsCSPContext::AsyncReportViolation(
     Element* aTriggeringElement, nsICSPEventListener* aCSPEventListener,
     nsIURI* aBlockedURI, BlockedContentSource aBlockedContentSource,
     nsIURI* aOriginalURI, const nsAString& aViolatedDirective,
     uint32_t aViolatedPolicyIndex, const nsAString& aObserverSubject,
     const nsAString& aSourceFile, const nsAString& aScriptSample,
     uint32_t aLineNum, uint32_t aColumnNum) {
+  EnsureIPCPoliciesRead();
   NS_ENSURE_ARG_MAX(aViolatedPolicyIndex, mPolicies.Length() - 1);
 
   nsCOMPtr<nsIRunnable> task = new CSPReportSenderRunnable(
       aTriggeringElement, aCSPEventListener, aBlockedURI, aBlockedContentSource,
       aOriginalURI, aViolatedPolicyIndex,
       mPolicies[aViolatedPolicyIndex]->getReportOnlyFlag(), aViolatedDirective,
       aObserverSubject, aSourceFile, aScriptSample, aLineNum, aColumnNum, this);
 
@@ -1334,16 +1365,17 @@ nsresult nsCSPContext::AsyncReportViolat
 
   NS_DispatchToMainThread(task.forget());
   return NS_OK;
 }
 
 NS_IMETHODIMP
 nsCSPContext::RequireSRIForType(nsContentPolicyType aContentType,
                                 bool* outRequiresSRIForType) {
+  EnsureIPCPoliciesRead();
   *outRequiresSRIForType = false;
   for (uint32_t i = 0; i < mPolicies.Length(); i++) {
     if (mPolicies[i]->hasDirective(REQUIRE_SRI_FOR)) {
       if (mPolicies[i]->requireSRIForType(aContentType)) {
         *outRequiresSRIForType = true;
         return NS_OK;
       }
     }
@@ -1490,16 +1522,17 @@ nsCSPContext::Permits(Element* aTriggeri
   return NS_OK;
 }
 
 NS_IMETHODIMP
 nsCSPContext::ToJSON(nsAString& outCSPinJSON) {
   outCSPinJSON.Truncate();
   dom::CSPPolicies jsonPolicies;
   jsonPolicies.mCsp_policies.Construct();
+  EnsureIPCPoliciesRead();
 
   for (uint32_t p = 0; p < mPolicies.Length(); p++) {
     dom::CSP jsonCSP;
     mPolicies[p]->toDomCSPStruct(jsonCSP);
     jsonPolicies.mCsp_policies.Value().AppendElement(jsonCSP, fallible);
   }
 
   // convert the gathered information to JSON
@@ -1511,16 +1544,17 @@ nsCSPContext::ToJSON(nsAString& outCSPin
 
 NS_IMETHODIMP
 nsCSPContext::GetCSPSandboxFlags(uint32_t* aOutSandboxFlags) {
   if (!aOutSandboxFlags) {
     return NS_ERROR_FAILURE;
   }
   *aOutSandboxFlags = SANDBOXED_NONE;
 
+  EnsureIPCPoliciesRead();
   for (uint32_t i = 0; i < mPolicies.Length(); i++) {
     uint32_t flags = mPolicies[i]->getSandboxFlags();
 
     // current policy doesn't have sandbox flag, check next policy
     if (!flags) {
       continue;
     }
 
@@ -1677,42 +1711,39 @@ nsCSPContext::Read(nsIObjectInputStream*
 
     bool reportOnly = false;
     rv = aStream->ReadBoolean(&reportOnly);
     NS_ENSURE_SUCCESS(rv, rv);
 
     bool deliveredViaMetaTag = false;
     rv = aStream->ReadBoolean(&deliveredViaMetaTag);
     NS_ENSURE_SUCCESS(rv, rv);
-
-    // @param deliveredViaMetaTag:
-    // when parsing the CSP policy string initially we already remove directives
-    // that should not be processed when delivered via the meta tag. Such
-    // directives will not be present at this point anymore.
-    nsCSPPolicy* policy = nsCSPParser::parseContentSecurityPolicy(
-        policyString, mSelfURI, reportOnly, this, deliveredViaMetaTag);
-    if (policy) {
-      mPolicies.AppendElement(policy);
-    }
+    mIPCPolicies.AppendElement(mozilla::ipc::ContentSecurityPolicy(
+        policyString, reportOnly, deliveredViaMetaTag));
   }
 
   return NS_OK;
 }
 
 NS_IMETHODIMP
 nsCSPContext::Write(nsIObjectOutputStream* aStream) {
   nsresult rv = NS_WriteOptionalCompoundObject(aStream, mSelfURI,
                                                NS_GET_IID(nsIURI), true);
   NS_ENSURE_SUCCESS(rv, rv);
 
   // Serialize all the policies.
-  aStream->Write32(mPolicies.Length());
+  aStream->Write32(mPolicies.Length() + mIPCPolicies.Length());
 
   nsAutoString polStr;
   for (uint32_t p = 0; p < mPolicies.Length(); p++) {
     polStr.Truncate();
     mPolicies[p]->toString(polStr);
     aStream->WriteWStringZ(polStr.get());
     aStream->WriteBoolean(mPolicies[p]->getReportOnlyFlag());
     aStream->WriteBoolean(mPolicies[p]->getDeliveredViaMetaTagFlag());
   }
+  for (auto& policy : mIPCPolicies) {
+    aStream->WriteWStringZ(policy.policy().get());
+    aStream->WriteBoolean(policy.reportOnlyFlag());
+    aStream->WriteBoolean(policy.deliveredViaMetaTagFlag());
+  }
   return NS_OK;
 }
--- a/dom/security/nsCSPContext.h
+++ b/dom/security/nsCSPContext.h
@@ -32,16 +32,19 @@
 class nsINetworkInterceptController;
 class nsIEventTarget;
 struct ConsoleMsgQueueElem;
 
 namespace mozilla {
 namespace dom {
 class Element;
 }
+namespace ipc {
+class ContentSecurityPolicy;
+}
 }  // namespace mozilla
 
 class nsCSPContext : public nsIContentSecurityPolicy {
  public:
   NS_DECL_ISUPPORTS
   NS_DECL_NSICONTENTSECURITYPOLICY
   NS_DECL_NSISERIALIZABLE
 
@@ -50,16 +53,19 @@ class nsCSPContext : public nsIContentSe
 
  public:
   nsCSPContext();
 
   nsresult InitFromOther(nsCSPContext* otherContext,
                          mozilla::dom::Document* aDoc,
                          nsIPrincipal* aPrincipal);
 
+  void SetIPCPolicies(
+      const nsTArray<mozilla::ipc::ContentSecurityPolicy>& policies);
+
   /**
    * SetRequestContext() needs to be called before the innerWindowID
    * is initialized on the document. Use this function to call back to
    * flush queued up console messages and initalize the innerWindowID.
    */
   void flushConsoleMessages();
 
   void logToConsole(const char* aName, const char16_t** aParams,
@@ -128,16 +134,18 @@ class nsCSPContext : public nsIContentSe
 
   static uint32_t ScriptSampleMaxLength() {
     return std::max(
         mozilla::StaticPrefs::security_csp_reporting_script_sample_max_length(),
         0);
   }
 
  private:
+  void EnsureIPCPoliciesRead();
+
   bool permitsInternal(CSPDirective aDir,
                        mozilla::dom::Element* aTriggeringElement,
                        nsICSPEventListener* aCSPEventListener,
                        nsIURI* aContentLocation, nsIURI* aOriginalURIIfRedirect,
                        const nsAString& aNonce, bool aIsPreload, bool aSpecific,
                        bool aSendViolationReports,
                        bool aSendContentLocationInViolationReports,
                        bool aParserCreated);
@@ -148,16 +156,22 @@ class nsCSPContext : public nsIContentSe
                              nsICSPEventListener* aCSPEventListener,
                              const nsAString& aNonce, const nsAString& aContent,
                              const nsAString& aViolatedDirective,
                              uint32_t aViolatedPolicyIndex,
                              uint32_t aLineNumber, uint32_t aColumnNumber);
 
   nsString mReferrer;
   uint64_t mInnerWindowID;  // used for web console logging
+  // When deserializing an nsCSPContext instance, we initially just keep the
+  // policies unparsed. We will only reconstruct actual CSP policy instances
+  // when there's an attempt to use the CSP. Given a better way to serialize/
+  // deserialize individual nsCSPPolicy objects, this performance
+  // optimization could go away.
+  nsTArray<mozilla::ipc::ContentSecurityPolicy> mIPCPolicies;
   nsTArray<nsCSPPolicy*> mPolicies;
   nsCOMPtr<nsIURI> mSelfURI;
   nsCOMPtr<nsILoadGroup> mCallingChannelLoadGroup;
   nsWeakPtr mLoadingContext;
   // The CSP hangs off the principal, so let's store a raw pointer of the
   // principal to avoid memory leaks. Within the destructor of the principal we
   // explicitly set mLoadingPrincipal to null.
   nsIPrincipal* mLoadingPrincipal;
--- a/gfx/wr/webrender/src/batch.rs
+++ b/gfx/wr/webrender/src/batch.rs
@@ -279,34 +279,36 @@ impl OpaqueBatchList {
             batch.instances.reverse();
         }
     }
 }
 
 pub struct BatchList {
     pub alpha_batch_list: AlphaBatchList,
     pub opaque_batch_list: OpaqueBatchList,
-    pub scissor_rect: Option<DeviceIntRect>,
+    /// A list of rectangle regions this batch should be drawn
+    /// in. Each region will have scissor rect set before drawing.
+    pub regions: Vec<DeviceIntRect>,
     pub tile_blits: Vec<TileBlit>,
 }
 
 impl BatchList {
     pub fn new(
         screen_size: DeviceIntSize,
-        scissor_rect: Option<DeviceIntRect>,
+        regions: Vec<DeviceIntRect>,
         tile_blits: Vec<TileBlit>,
     ) -> Self {
         // The threshold for creating a new batch is
         // one quarter the screen size.
         let batch_area_threshold = (screen_size.width * screen_size.height) as f32 / 4.0;
 
         BatchList {
             alpha_batch_list: AlphaBatchList::new(),
             opaque_batch_list: OpaqueBatchList::new(batch_area_threshold),
-            scissor_rect,
+            regions,
             tile_blits,
         }
     }
 
     pub fn push_single_instance(
         &mut self,
         key: BatchKey,
         bounding_rect: &PictureRect,
@@ -376,28 +378,35 @@ impl PrimitiveBatch {
     }
 }
 
 #[cfg_attr(feature = "capture", derive(Serialize))]
 #[cfg_attr(feature = "replay", derive(Deserialize))]
 pub struct AlphaBatchContainer {
     pub opaque_batches: Vec<PrimitiveBatch>,
     pub alpha_batches: Vec<PrimitiveBatch>,
-    pub scissor_rect: Option<DeviceIntRect>,
+    /// The overall scissor rect for this render task, if one
+    /// is required.
+    pub task_scissor_rect: Option<DeviceIntRect>,
+    /// A list of rectangle regions this batch should be drawn
+    /// in. Each region will have scissor rect set before drawing.
+    pub regions: Vec<DeviceIntRect>,
     pub tile_blits: Vec<TileBlit>,
 }
 
 impl AlphaBatchContainer {
     pub fn new(
-        scissor_rect: Option<DeviceIntRect>,
+        task_scissor_rect: Option<DeviceIntRect>,
+        regions: Vec<DeviceIntRect>,
     ) -> AlphaBatchContainer {
         AlphaBatchContainer {
             opaque_batches: Vec::new(),
             alpha_batches: Vec::new(),
-            scissor_rect,
+            task_scissor_rect,
+            regions,
             tile_blits: Vec::new(),
         }
     }
 
     pub fn is_empty(&self) -> bool {
         self.opaque_batches.is_empty() &&
         self.alpha_batches.is_empty()
     }
@@ -447,68 +456,59 @@ struct SegmentInstanceData {
     textures: BatchTextures,
     user_data: i32,
 }
 
 /// Encapsulates the logic of building batches for items that are blended.
 pub struct AlphaBatchBuilder {
     pub batch_lists: Vec<BatchList>,
     screen_size: DeviceIntSize,
-    scissor_rect: Option<DeviceIntRect>,
+    task_scissor_rect: Option<DeviceIntRect>,
     glyph_fetch_buffer: Vec<GlyphFetchResult>,
 }
 
 impl AlphaBatchBuilder {
     pub fn new(
         screen_size: DeviceIntSize,
-        scissor_rect: Option<DeviceIntRect>,
+        task_scissor_rect: Option<DeviceIntRect>,
     ) -> Self {
         let batch_lists = vec![
             BatchList::new(
                 screen_size,
-                scissor_rect,
+                Vec::new(),
                 Vec::new(),
             ),
         ];
 
         AlphaBatchBuilder {
             batch_lists,
-            scissor_rect,
+            task_scissor_rect,
             screen_size,
             glyph_fetch_buffer: Vec::new(),
         }
     }
 
     fn push_new_batch_list(
         &mut self,
-        scissor_rect: Option<DeviceIntRect>,
+        regions: Vec<DeviceIntRect>,
         tile_blits: Vec<TileBlit>,
     ) {
-        let scissor_rect = match (scissor_rect, self.scissor_rect) {
-            (Some(rect0), Some(rect1)) => {
-                Some(rect0.intersection(&rect1).unwrap_or(DeviceIntRect::zero()))
-            }
-            (Some(rect0), None) => Some(rect0),
-            (None, Some(rect1)) => Some(rect1),
-            (None, None) => None,
-        };
-
         self.batch_lists.push(BatchList::new(
             self.screen_size,
-            scissor_rect,
+            regions,
             tile_blits,
         ));
     }
 
     fn current_batch_list(&mut self) -> &mut BatchList {
         self.batch_lists.last_mut().unwrap()
     }
 
     fn can_merge(&self) -> bool {
-        self.scissor_rect.is_none() &&
+        self.task_scissor_rect.is_none() &&
         self.batch_lists.len() == 1
     }
 
     pub fn build(
         mut self,
         batch_containers: &mut Vec<AlphaBatchContainer>,
         merged_batches: &mut AlphaBatchContainer,
     ) {
@@ -520,17 +520,18 @@ impl AlphaBatchBuilder {
             let batch_list = self.batch_lists.pop().unwrap();
             debug_assert!(batch_list.tile_blits.is_empty());
             merged_batches.merge(batch_list);
         } else {
             for batch_list in self.batch_lists {
                 batch_containers.push(AlphaBatchContainer {
                     alpha_batches: batch_list.alpha_batch_list.batches,
                     opaque_batches: batch_list.opaque_batch_list.batches,
-                    scissor_rect: batch_list.scissor_rect,
+                    task_scissor_rect: self.task_scissor_rect,
+                    regions: batch_list.regions,
                     tile_blits: batch_list.tile_blits,
                 });
             }
         }
     }
 
     pub fn add_pic_to_batch(
         &mut self,
@@ -1118,35 +1119,44 @@ impl AlphaBatchBuilder {
                                             z_id,
                                         );
 
                                         batch.push(PrimitiveInstanceData::from(instance));
                                     }
 
                                     // If there is a dirty rect for the tile cache, recurse into the
                                     // main picture primitive list, and draw them first.
-                                    if let Some(ref dirty_region) = tile_cache.dirty_region {
+                                    if !tile_cache.dirty_region.is_empty() {
                                         let mut tile_blits = Vec::new();
 
                                         let (target_rect, _) = render_tasks[task_id].get_target_rect();
 
                                         for blit in &tile_cache.pending_blits {
                                             tile_blits.push(TileBlit {
                                                 dest_offset: blit.dest_offset,
                                                 size: blit.size,
                                                 target: blit.target.clone(),
                                                 src_offset: DeviceIntPoint::new(
                                                     blit.src_offset.x + target_rect.origin.x,
                                                     blit.src_offset.y + target_rect.origin.y,
                                                 ),
                                             })
                                         }
 
+                                        // Collect the list of regions to scissor and repeat
+                                        // the draw calls into, based on dirty rects.
+                                        let batch_regions = tile_cache
+                                            .dirty_region
+                                            .dirty_rects
+                                            .iter()
+                                            .map(|dirty_rect| dirty_rect.device_rect)
+                                            .collect();
+
                                         self.push_new_batch_list(
-                                            Some(dirty_region.dirty_device_rect),
+                                            batch_regions,
                                             tile_blits,
                                         );
 
                                         self.add_pic_to_batch(
                                             picture,
                                             task_id,
                                             ctx,
                                             gpu_cache,
@@ -1154,17 +1164,17 @@ impl AlphaBatchBuilder {
                                             deferred_resolves,
                                             prim_headers,
                                             transforms,
                                             root_spatial_node_index,
                                             z_generator,
                                         );
 
                                         self.push_new_batch_list(
-                                            None,
+                                            Vec::new(),
                                             Vec::new(),
                                         );
                                     }
                                 }
                             }
                             PictureCompositeMode::Filter(filter) => {
                                 let surface = ctx.surfaces[raster_config.surface_index.0]
                                     .surface
--- a/gfx/wr/webrender/src/device/gl.rs
+++ b/gfx/wr/webrender/src/device/gl.rs
@@ -1004,16 +1004,48 @@ impl<'a> DrawTarget<'a> {
 
     /// Returns the dimensions of this draw-target.
     pub fn dimensions(&self) -> DeviceIntSize {
         match *self {
             DrawTarget::Default(d) => d,
             DrawTarget::Texture { texture, .. } => texture.get_dimensions(),
         }
     }
+
+    /// Given a scissor rect, convert it to the right coordinate space
+    /// depending on the draw target kind. If no scissor rect was supplied,
+    /// returns a scissor rect that encloses the entire render target.
+    pub fn build_scissor_rect(
+        &self,
+        scissor_rect: Option<DeviceIntRect>,
+        framebuffer_target_rect: DeviceIntRect,
+    ) -> DeviceIntRect {
+        let dimensions = self.dimensions();
+
+        match scissor_rect {
+            Some(scissor_rect) => {
+                // Note: `framebuffer_target_rect` needs a Y-flip before going to GL
+                if self.is_default() {
+                    let mut rect = scissor_rect
+                        .intersection(&framebuffer_target_rect.to_i32())
+                        .unwrap_or(DeviceIntRect::zero());
+                    rect.origin.y = dimensions.height as i32 - rect.origin.y - rect.size.height;
+                    rect
+                } else {
+                    scissor_rect
+                }
+            }
+            None => {
+                DeviceIntRect::new(
+                    DeviceIntPoint::zero(),
+                    dimensions,
+                )
+            }
+        }
+    }
 }
 
 /// Contains the parameters necessary to bind a texture-backed read target.
 #[derive(Clone, Copy)]
 pub enum ReadTarget<'a> {
     /// Use the device's default draw target.
     Default,
     /// Use the provided texture,
--- a/gfx/wr/webrender/src/frame_builder.rs
+++ b/gfx/wr/webrender/src/frame_builder.rs
@@ -8,17 +8,17 @@ use api::{LayoutPoint, LayoutRect, Layou
 use clip::{ClipDataStore, ClipStore};
 use clip_scroll_tree::{ClipScrollTree, ROOT_SPATIAL_NODE_INDEX, SpatialNodeIndex};
 use display_list_flattener::{DisplayListFlattener};
 use gpu_cache::GpuCache;
 use gpu_types::{PrimitiveHeaders, TransformPalette, UvRectKind, ZBufferIdGenerator};
 use hit_test::{HitTester, HitTestingRun};
 use internal_types::{FastHashMap, PlaneSplitter};
 use picture::{PictureSurface, PictureUpdateState, SurfaceInfo, ROOT_SURFACE_INDEX, SurfaceIndex};
-use picture::{RetainedTiles, TileCache};
+use picture::{RetainedTiles, TileCache, DirtyRegion};
 use prim_store::{PrimitiveStore, SpaceMapper, PictureIndex, PrimitiveDebugId, PrimitiveScratchBuffer};
 #[cfg(feature = "replay")]
 use prim_store::{PrimitiveStoreStats};
 use profiler::{FrameProfileCounters, GpuCacheProfileCounters, TextureCacheProfileCounters};
 use render_backend::{DataStores, FrameStamp};
 use render_task::{RenderTask, RenderTaskId, RenderTaskLocation, RenderTaskTree};
 use resource_cache::{ResourceCache};
 use scene::{ScenePipeline, SceneProperties};
@@ -103,32 +103,49 @@ pub struct FrameBuildingState<'a> {
     pub render_tasks: &'a mut RenderTaskTree,
     pub profile_counters: &'a mut FrameProfileCounters,
     pub clip_store: &'a mut ClipStore,
     pub resource_cache: &'a mut ResourceCache,
     pub gpu_cache: &'a mut GpuCache,
     pub transforms: &'a mut TransformPalette,
     pub segment_builder: SegmentBuilder,
     pub surfaces: &'a mut Vec<SurfaceInfo>,
+    pub dirty_region_stack: Vec<DirtyRegion>,
+}
+
+impl<'a> FrameBuildingState<'a> {
+    /// Retrieve the current dirty region during primitive traversal.
+    pub fn current_dirty_region(&self) -> &DirtyRegion {
+        self.dirty_region_stack.last().unwrap()
+    }
+
+    /// Push a new dirty region for child primitives to cull / clip against.
+    pub fn push_dirty_region(&mut self, region: DirtyRegion) {
+        self.dirty_region_stack.push(region);
+    }
+
+    /// Pop the top dirty region from the stack.
+    pub fn pop_dirty_region(&mut self) {
+        self.dirty_region_stack.pop().unwrap();
+    }
 }
 
 /// Immutable context of a picture when processing children.
 #[derive(Debug)]
 pub struct PictureContext {
     pub pic_index: PictureIndex,
     pub pipeline_id: PipelineId,
     pub apply_local_clip_rect: bool,
     pub allow_subpixel_aa: bool,
     pub is_passthrough: bool,
     pub raster_space: RasterSpace,
     pub surface_spatial_node_index: SpatialNodeIndex,
     pub raster_spatial_node_index: SpatialNodeIndex,
     /// The surface that this picture will render on.
     pub surface_index: SurfaceIndex,
-    pub dirty_world_rect: WorldRect,
 }
 
 /// Mutable state of a picture that gets modified when
 /// the children are processed.
 pub struct PictureState {
     pub is_cacheable: bool,
     pub map_local_to_pic: SpaceMapper<LayoutPixel, PicturePixel>,
     pub map_pic_to_world: SpaceMapper<PicturePixel, WorldPixel>,
@@ -341,30 +358,40 @@ impl FrameBuilder {
             render_tasks,
             profile_counters,
             clip_store: &mut self.clip_store,
             resource_cache,
             gpu_cache,
             transforms: transform_palette,
             segment_builder: SegmentBuilder::new(),
             surfaces: pic_update_state.surfaces,
+            dirty_region_stack: Vec::new(),
         };
 
+        // Push a default dirty region which culls primitives
+        // against the screen world rect, in absence of any
+        // other dirty regions.
+        let mut default_dirty_region = DirtyRegion::new();
+        default_dirty_region.push(
+            frame_context.screen_world_rect,
+            frame_context.device_pixel_scale,
+        );
+        frame_state.push_dirty_region(default_dirty_region);
+
         let (pic_context, mut pic_state, mut prim_list) = self
             .prim_store
             .pictures[self.root_pic_index.0]
             .take_context(
                 self.root_pic_index,
                 root_spatial_node_index,
                 root_spatial_node_index,
                 ROOT_SURFACE_INDEX,
                 true,
                 &mut frame_state,
                 &frame_context,
-                screen_world_rect,
             )
             .unwrap();
 
         self.prim_store.prepare_primitives(
             &mut prim_list,
             &pic_context,
             &mut pic_state,
             &frame_context,
@@ -373,18 +400,21 @@ impl FrameBuilder {
             scratch,
         );
 
         let pic = &mut self.prim_store.pictures[self.root_pic_index.0];
         pic.restore_context(
             prim_list,
             pic_context,
             pic_state,
+            &mut frame_state,
         );
 
+        frame_state.pop_dirty_region();
+
         let child_tasks = frame_state
             .surfaces[ROOT_SURFACE_INDEX.0]
             .take_render_tasks();
 
         let root_render_task = RenderTask::new_picture(
             RenderTaskLocation::Fixed(self.screen_rect.to_i32()),
             self.screen_rect.size.to_f32(),
             self.root_pic_index,
--- a/gfx/wr/webrender/src/picture.rs
+++ b/gfx/wr/webrender/src/picture.rs
@@ -95,16 +95,17 @@ pub type TileSize = TypedSize2D<i32, Til
 pub struct TileIndex(pub usize);
 
 /// The size in device pixels of a cached tile. The currently chosen
 /// size is arbitrary. We should do some profiling to find the best
 /// size for real world pages.
 pub const TILE_SIZE_WIDTH: i32 = 1024;
 pub const TILE_SIZE_HEIGHT: i32 = 256;
 const FRAMES_BEFORE_CACHING: usize = 2;
+const MAX_DIRTY_RECTS: usize = 3;
 
 /// The maximum size per axis of texture cache item,
 ///  in WorldPixel coordinates.
 // TODO(gw): This size is quite arbitrary - we should do some
 //           profiling / telemetry to see when it makes sense
 //           to cache a picture.
 const MAX_CACHE_SIZE: f32 = 2048.0;
 /// The maximum size per axis of a surface,
@@ -181,16 +182,19 @@ pub struct Tile {
     /// The set of transforms that affect primitives on this tile we
     /// care about. Stored as a set here, and then collected, sorted
     /// and converted to transform key values during post_update.
     transforms: FastHashSet<SpatialNodeIndex>,
     /// A list of potentially important clips. We can't know if
     /// they were important or can be discarded until we know the
     /// tile cache bounding rect.
     potential_clips: FastHashMap<RectangleKey, SpatialNodeIndex>,
+    /// If true, this tile should still be considered as part of
+    /// the dirty rect calculations.
+    consider_for_dirty_rect: bool,
 }
 
 impl Tile {
     /// Construct a new, invalid tile.
     fn new(
         id: TileId,
     ) -> Self {
         Tile {
@@ -201,16 +205,17 @@ impl Tile {
             handle: TextureCacheHandle::invalid(),
             descriptor: TileDescriptor::new(),
             is_same_content: false,
             is_valid: false,
             same_frames: 0,
             transforms: FastHashSet::default(),
             potential_clips: FastHashMap::default(),
             id,
+            consider_for_dirty_rect: false,
         }
     }
 
     /// Clear the dependencies for a tile.
     fn clear(&mut self) {
         self.transforms.clear();
         self.descriptor.clear();
         self.potential_clips.clear();
@@ -333,40 +338,204 @@ impl TileDescriptor {
         if !self.transforms.is_valid() {
             return false;
         }
 
         true
     }
 }
 
+/// Stores both the world and devices rects for a single dirty rect.
+#[derive(Debug, Clone)]
+pub struct DirtyRegionRect {
+    pub world_rect: WorldRect,
+    pub device_rect: DeviceIntRect,
+}
+
 /// Represents the dirty region of a tile cache picture.
-/// In future, we will want to support multiple dirty
-/// regions.
-#[derive(Debug)]
+#[derive(Debug, Clone)]
 pub struct DirtyRegion {
-    pub dirty_world_rect: WorldRect,
-    pub dirty_device_rect: DeviceIntRect,
+    /// The individual dirty rects of this region.
+    pub dirty_rects: Vec<DirtyRegionRect>,
+
+    /// The overall dirty rect, a combination of dirty_rects
+    pub combined: DirtyRegionRect,
+}
+
+impl DirtyRegion {
+    /// Construct a new dirty region tracker.
+    pub fn new() -> Self {
+        DirtyRegion {
+            dirty_rects: Vec::with_capacity(MAX_DIRTY_RECTS),
+            combined: DirtyRegionRect {
+                world_rect: WorldRect::zero(),
+                device_rect: DeviceIntRect::zero(),
+            },
+        }
+    }
+
+    /// Reset the dirty regions back to empty
+    pub fn clear(&mut self) {
+        self.dirty_rects.clear();
+        self.combined = DirtyRegionRect {
+            world_rect: WorldRect::zero(),
+            device_rect: DeviceIntRect::zero(),
+        }
+    }
+
+    /// Push a dirty rect into this region
+    pub fn push(
+        &mut self,
+        rect: WorldRect,
+        device_pixel_scale: DevicePixelScale,
+    ) {
+        let device_rect = (rect * device_pixel_scale).round().to_i32();
+
+        // Include this in the overall dirty rect
+        self.combined.world_rect = self.combined.world_rect.union(&rect);
+        self.combined.device_rect = self.combined.device_rect.union(&device_rect);
+
+        // Store the individual dirty rect.
+        self.dirty_rects.push(DirtyRegionRect {
+            world_rect: rect,
+            device_rect,
+        });
+    }
+
+    /// Returns true if this region has no dirty rects
+    pub fn is_empty(&self) -> bool {
+        self.dirty_rects.is_empty()
+    }
+
+    /// Collapse all dirty rects into a single dirty rect.
+    pub fn collapse(&mut self) {
+        self.dirty_rects.clear();
+        self.dirty_rects.push(self.combined.clone());
+    }
+}
+
+/// A helper struct to build a (roughly) minimal set of dirty rectangles
+/// from a list of individual dirty rectangles. This minimizes the number
+/// of scissors rects and batch resubmissions that are needed.
+struct DirtyRegionBuilder<'a> {
+    tiles: &'a mut [Tile],
+    tile_count: TileSize,
+    device_pixel_scale: DevicePixelScale,
+}
+
+impl<'a> DirtyRegionBuilder<'a> {
+    fn new(
+        tiles: &'a mut [Tile],
+        tile_count: TileSize,
+        device_pixel_scale: DevicePixelScale,
+    ) -> Self {
+        DirtyRegionBuilder {
+            tiles,
+            tile_count,
+            device_pixel_scale,
+        }
+    }
+
+    fn tile_index(&self, x: i32, y: i32) -> usize {
+        (y * self.tile_count.width + x) as usize
+    }
+
+    fn is_dirty(&self, x: i32, y: i32) -> bool {
+        if x == self.tile_count.width || y == self.tile_count.height {
+            return false;
+        }
+
+        self.get_tile(x, y).consider_for_dirty_rect
+    }
+
+    fn get_tile(&self, x: i32, y: i32) -> &Tile {
+        &self.tiles[self.tile_index(x, y)]
+    }
+
+    fn get_tile_mut(&mut self, x: i32, y: i32) -> &mut Tile {
+        &mut self.tiles[self.tile_index(x, y)]
+    }
+
+    /// Return true if the entire column is dirty
+    fn column_is_dirty(&self, x: i32, y0: i32, y1: i32) -> bool {
+        for y in y0 .. y1 {
+            if !self.is_dirty(x, y) {
+                return false;
+            }
+        }
+
+        true
+    }
+
+    /// Push a dirty rect into the final region list.
+    fn push_dirty_rect(
+        &mut self,
+        x0: i32,
+        y0: i32,
+        x1: i32,
+        y1: i32,
+        dirty_region: &mut DirtyRegion,
+    ) {
+        // Construct the overall dirty rect by combining the visible
+        // parts of the dirty rects that were combined.
+        let mut dirty_world_rect = WorldRect::zero();
+
+        for y in y0 .. y1 {
+            for x in x0 .. x1 {
+                let tile = self.get_tile_mut(x, y);
+                tile.consider_for_dirty_rect = false;
+                if let Some(visible_rect) = tile.visible_rect {
+                    dirty_world_rect = dirty_world_rect.union(&visible_rect);
+                }
+            }
+        }
+
+        dirty_region.push(dirty_world_rect, self.device_pixel_scale);
+    }
+
+    /// Simple sweep through the tile grid to try and coalesce individual
+    /// dirty rects into a smaller number of larger dirty rectangles.
+    fn build(&mut self, dirty_region: &mut DirtyRegion) {
+        for x0 in 0 .. self.tile_count.width {
+            for y0 in 0 .. self.tile_count.height {
+                let mut y1 = y0;
+
+                while self.is_dirty(x0, y1) {
+                    y1 += 1;
+                }
+
+                if y1 > y0 {
+                    let mut x1 = x0;
+
+                    while self.column_is_dirty(x1, y0, y1) {
+                        x1 += 1;
+                    }
+
+                    self.push_dirty_rect(x0, y0, x1, y1, dirty_region);
+                }
+            }
+        }
+    }
 }
 
 /// Represents a cache of tiles that make up a picture primitives.
 pub struct TileCache {
     /// The positioning node for this tile cache.
     spatial_node_index: SpatialNodeIndex,
     /// List of tiles present in this picture (stored as a 2D array)
     pub tiles: Vec<Tile>,
     /// A helper struct to map local rects into world coords.
     map_local_to_world: SpaceMapper<LayoutPixel, WorldPixel>,
     /// A list of tiles to draw during batching.
     pub tiles_to_draw: Vec<TileIndex>,
     /// List of opacity bindings, with some extra information
     /// about whether they changed since last frame.
     opacity_bindings: FastHashMap<PropertyBindingId, OpacityBindingInfo>,
-    /// If Some(..) the region that is dirty in this picture.
-    pub dirty_region: Option<DirtyRegion>,
+    /// The current dirty region tracker for this picture.
+    pub dirty_region: DirtyRegion,
     /// If true, we need to update the prim dependencies, due
     /// to relative transforms changing. The dependencies are
     /// stored in each tile, and are a list of things that
     /// force the tile to re-rasterize if they change (e.g.
     /// images, transforms).
     needs_update: bool,
     /// The current world reference point that tiles are created around.
     world_origin: WorldPoint,
@@ -501,17 +670,17 @@ impl TileCache {
             spatial_node_index,
             tiles: Vec::new(),
             map_local_to_world: SpaceMapper::new(
                 ROOT_SPATIAL_NODE_INDEX,
                 WorldRect::zero(),
             ),
             tiles_to_draw: Vec::new(),
             opacity_bindings: FastHashMap::default(),
-            dirty_region: None,
+            dirty_region: DirtyRegion::new(),
             needs_update: true,
             world_origin: WorldPoint::zero(),
             world_tile_size: WorldSize::zero(),
             tile_count: TileSize::zero(),
             scroll_offset: None,
             pending_blits: Vec::new(),
             world_bounding_rect: WorldRect::zero(),
             root_clip_rect: WorldRect::max_rect(),
@@ -1132,19 +1301,17 @@ impl TileCache {
     /// set of tile blits.
     pub fn post_update(
         &mut self,
         resource_cache: &mut ResourceCache,
         gpu_cache: &mut GpuCache,
         frame_context: &FrameVisibilityContext,
         _scratch: &mut PrimitiveScratchBuffer,
     ) -> LayoutRect {
-        let mut dirty_world_rect = WorldRect::zero();
-
-        self.dirty_region = None;
+        self.dirty_region.clear();
         self.pending_blits.clear();
 
         let descriptor = ImageDescriptor::new(
             TILE_SIZE_WIDTH,
             TILE_SIZE_HEIGHT,
             ImageFormat::BGRA8,
             true,
             false,
@@ -1228,16 +1395,18 @@ impl TileCache {
 
             // If there are no primitives there is no need to draw or cache it.
             if tile.descriptor.prims.is_empty() {
                 continue;
             }
 
             // Decide how to handle this tile when drawing this frame.
             if tile.is_valid {
+                // No need to include this is any dirty rect calculations.
+                tile.consider_for_dirty_rect = false;
                 self.tiles_to_draw.push(TileIndex(i));
 
                 #[cfg(feature = "debug_renderer")]
                 {
                     if frame_context.debug_flags.contains(DebugFlags::PICTURE_CACHING_DBG) {
                         let tile_device_rect = tile.world_rect * frame_context.device_pixel_scale;
                         let mut label_pos = tile_device_rect.origin + DeviceVector2D::new(20.0, 30.0);
                         _scratch.push_debug_rect(
@@ -1253,19 +1422,16 @@ impl TileCache {
                         _scratch.push_debug_string(
                             label_pos,
                             debug_colors::RED,
                             format!("same: {} frames", tile.same_frames),
                         );
                     }
                 }
             } else {
-                // Add the tile rect to the dirty rect.
-                dirty_world_rect = dirty_world_rect.union(&visible_rect);
-
                 #[cfg(feature = "debug_renderer")]
                 {
                     if frame_context.debug_flags.contains(DebugFlags::PICTURE_CACHING_DBG) {
                         _scratch.push_debug_rect(
                             visible_rect * frame_context.device_pixel_scale,
                             debug_colors::RED,
                         );
                     }
@@ -1308,30 +1474,40 @@ impl TileCache {
                         src_offset: src_origin,
                         dest_offset: dest_rect.origin,
                         size: dest_rect.size,
                     });
 
                     // We can consider this tile valid now.
                     tile.is_valid = true;
                 }
+
+                // This tile should be considered as part of the dirty rect calculations.
+                tile.consider_for_dirty_rect = true;
             }
         }
 
-        // Store the dirty region for drawing the main scene.
-        self.dirty_region = if dirty_world_rect.is_empty() {
-            None
-        } else {
-            let dirty_device_rect = dirty_world_rect * frame_context.device_pixel_scale;
-
-            Some(DirtyRegion {
-                dirty_world_rect,
-                dirty_device_rect: dirty_device_rect.round().to_i32(),
-            })
-        };
+        // Build a minimal set of dirty rects from the set of dirty tiles that
+        // were found above.
+        let mut builder = DirtyRegionBuilder::new(
+            &mut self.tiles,
+            self.tile_count,
+            frame_context.device_pixel_scale,
+        );
+
+        builder.build(&mut self.dirty_region);
+
+        // If we end up with too many dirty rects, then it's going to be a lot
+        // of extra draw calls to submit (since we currently just submit every
+        // draw call for every dirty rect). In this case, bail out and work
+        // with a single, large dirty rect. In future we can consider improving
+        // on this by supporting batching per dirty region.
+        if self.dirty_region.dirty_rects.len() > MAX_DIRTY_RECTS {
+            self.dirty_region.collapse();
+        }
 
         local_clip_rect
     }
 }
 
 /// Maintains a stack of picture and surface information, that
 /// is used during the initial picture traversal.
 pub struct PictureUpdateState<'a> {
@@ -1928,42 +2104,21 @@ impl PicturePrimitive {
         &mut self,
         pic_index: PictureIndex,
         surface_spatial_node_index: SpatialNodeIndex,
         raster_spatial_node_index: SpatialNodeIndex,
         surface_index: SurfaceIndex,
         parent_allows_subpixel_aa: bool,
         frame_state: &mut FrameBuildingState,
         frame_context: &FrameBuildingContext,
-        dirty_world_rect: WorldRect,
     ) -> Option<(PictureContext, PictureState, PrimitiveList)> {
         if !self.is_visible() {
             return None;
         }
 
-        // Work out the dirty world rect for this picture.
-        let dirty_world_rect = match self.tile_cache {
-            Some(ref tile_cache) => {
-                // If a tile cache is present, extract the dirty
-                // world rect from the dirty region. If there is
-                // no dirty region there is nothing to render.
-                // TODO(gw): We could early out here in that case?
-                tile_cache
-                    .dirty_region
-                    .as_ref()
-                    .map_or(WorldRect::zero(), |region| {
-                        region.dirty_world_rect
-                    })
-            }
-            None => {
-                // No tile cache - just assume the current dirty world rect.
-                dirty_world_rect
-            }
-        };
-
         // Extract the raster and surface spatial nodes from the raster
         // config, if this picture establishes a surface. Otherwise just
         // pass in the spatial node indices from the parent context.
         let (raster_spatial_node_index, surface_spatial_node_index, surface_index) = match self.raster_config {
             Some(ref raster_config) => {
                 let surface = &frame_state.surfaces[raster_config.surface_index.0];
 
                 (surface.raster_spatial_node_index, self.spatial_node_index, raster_config.surface_index)
@@ -1971,32 +2126,32 @@ impl PicturePrimitive {
             None => {
                 (raster_spatial_node_index, surface_spatial_node_index, surface_index)
             }
         };
 
         let map_pic_to_world = SpaceMapper::new_with_target(
             ROOT_SPATIAL_NODE_INDEX,
             surface_spatial_node_index,
-            dirty_world_rect,
+            frame_context.screen_world_rect,
             frame_context.clip_scroll_tree,
         );
 
         let pic_bounds = map_pic_to_world.unmap(&map_pic_to_world.bounds)
                                          .unwrap_or(PictureRect::max_rect());
 
         let map_local_to_pic = SpaceMapper::new(
             surface_spatial_node_index,
             pic_bounds,
         );
 
         let (map_raster_to_world, map_pic_to_raster) = create_raster_mappers(
             surface_spatial_node_index,
             raster_spatial_node_index,
-            dirty_world_rect,
+            frame_context.screen_world_rect,
             frame_context.clip_scroll_tree,
         );
 
         let plane_splitter = match self.context_3d {
             Picture3DContext::Out => {
                 None
             }
             Picture3DContext::In { root_data: Some(_), .. } => {
@@ -2040,30 +2195,41 @@ impl PicturePrimitive {
             pipeline_id: self.pipeline_id,
             apply_local_clip_rect: self.apply_local_clip_rect,
             allow_subpixel_aa,
             is_passthrough: self.raster_config.is_none(),
             raster_space: self.requested_raster_space,
             raster_spatial_node_index,
             surface_spatial_node_index,
             surface_index,
-            dirty_world_rect,
         };
 
+        // If this is a picture cache, push the dirty region to ensure any
+        // child primitives are culled and clipped to the dirty rect(s).
+        if let Some(ref tile_cache) = self.tile_cache {
+            frame_state.push_dirty_region(tile_cache.dirty_region.clone());
+        }
+
         let prim_list = mem::replace(&mut self.prim_list, PrimitiveList::empty());
 
         Some((context, state, prim_list))
     }
 
     pub fn restore_context(
         &mut self,
         prim_list: PrimitiveList,
         context: PictureContext,
         state: PictureState,
+        frame_state: &mut FrameBuildingState,
     ) {
+        // Pop the dirty region for this picture cache
+        if self.tile_cache.is_some() {
+            frame_state.pop_dirty_region();
+        }
+
         self.prim_list = prim_list;
         self.state = Some((state, context));
     }
 
     pub fn take_state_and_context(&mut self) -> (PictureState, PictureContext) {
         self.state.take().expect("bug: no state present!")
     }
 
@@ -2446,17 +2612,17 @@ impl PicturePrimitive {
             let surface_info = &mut frame_state.surfaces[raster_config.surface_index.0];
             (surface_info.raster_spatial_node_index, surface_info.take_render_tasks())
         };
         let surfaces = &mut frame_state.surfaces;
 
         let (map_raster_to_world, map_pic_to_raster) = create_raster_mappers(
             prim_instance.spatial_node_index,
             raster_spatial_node_index,
-            pic_context.dirty_world_rect,
+            frame_context.screen_world_rect,
             frame_context.clip_scroll_tree,
         );
 
         let pic_rect = PictureRect::from_untyped(&self.local_rect.to_untyped());
 
         let (clipped, unclipped) = match get_raster_rects(
             pic_rect,
             &map_pic_to_raster,
@@ -2890,27 +3056,27 @@ fn calculate_uv_rect_kind(
         bottom_left,
         bottom_right,
     }
 }
 
 fn create_raster_mappers(
     surface_spatial_node_index: SpatialNodeIndex,
     raster_spatial_node_index: SpatialNodeIndex,
-    dirty_world_rect: WorldRect,
+    world_rect: WorldRect,
     clip_scroll_tree: &ClipScrollTree,
 ) -> (SpaceMapper<RasterPixel, WorldPixel>, SpaceMapper<PicturePixel, RasterPixel>) {
     let map_raster_to_world = SpaceMapper::new_with_target(
         ROOT_SPATIAL_NODE_INDEX,
         raster_spatial_node_index,
-        dirty_world_rect,
+        world_rect,
         clip_scroll_tree,
     );
 
-    let raster_bounds = map_raster_to_world.unmap(&dirty_world_rect)
+    let raster_bounds = map_raster_to_world.unmap(&world_rect)
                                            .unwrap_or(RasterRect::max_rect());
 
     let map_pic_to_raster = SpaceMapper::new_with_target(
         raster_spatial_node_index,
         surface_spatial_node_index,
         raster_bounds,
         clip_scroll_tree,
     );
--- a/gfx/wr/webrender/src/prim_store/mod.rs
+++ b/gfx/wr/webrender/src/prim_store/mod.rs
@@ -2198,17 +2198,16 @@ impl PrimitiveStore {
                     match pic.take_context(
                         pic_index,
                         pic_context.surface_spatial_node_index,
                         pic_context.raster_spatial_node_index,
                         pic_context.surface_index,
                         pic_context.allow_subpixel_aa,
                         frame_state,
                         frame_context,
-                        pic_context.dirty_world_rect,
                     ) {
                         Some(info) => Some(info),
                         None => {
                             if prim_instance.is_chased() {
                                 println!("\tculled for carrying an invisible composite filter");
                             }
 
                             return false;
@@ -2250,16 +2249,17 @@ impl PrimitiveStore {
                 }
 
                 // Restore the dependencies (borrow check dance)
                 self.pictures[pic_context_for_children.pic_index.0]
                     .restore_context(
                         prim_list,
                         pic_context_for_children,
                         pic_state_for_children,
+                        frame_state,
                     );
 
                 is_passthrough
             }
             None => {
                 false
             }
         };
@@ -2306,17 +2306,17 @@ impl PrimitiveStore {
                 ) {
                     if let Some(ref mut splitter) = pic_state.plane_splitter {
                         PicturePrimitive::add_split_plane(
                             splitter,
                             frame_state.transforms,
                             prim_instance,
                             pic.local_rect,
                             &prim_info.combined_local_clip_rect,
-                            pic_context.dirty_world_rect,
+                            frame_context.screen_world_rect,
                             plane_split_anchor,
                         );
                     }
                 } else {
                     prim_instance.visibility_info = PrimitiveVisibilityIndex::INVALID;
                 }
 
                 if let Some(mut request) = frame_state.gpu_cache.request(&mut pic.gpu_location) {
@@ -2372,22 +2372,46 @@ impl PrimitiveStore {
 
             // The original clipped world rect was calculated during the initial visibility pass.
             // However, it's possible that the dirty rect has got smaller, if tiles were not
             // dirty. Intersecting with the dirty rect here eliminates preparing any primitives
             // outside the dirty rect, and reduces the size of any off-screen surface allocations
             // for clip masks / render tasks that we make.
             {
                 let visibility_info = &mut scratch.prim_info[prim_instance.visibility_info.0 as usize];
-
-                match visibility_info.clipped_world_rect.intersection(&pic_context.dirty_world_rect) {
+                let dirty_region = frame_state.current_dirty_region();
+
+                // Check if the primitive world rect intersects with the overall dirty rect first.
+                match visibility_info.clipped_world_rect.intersection(&dirty_region.combined.world_rect) {
                     Some(rect) => {
+                        // It does intersect the overall dirty rect, so it *might* be visible.
+                        // Store this reduced rect here, which is used for clip mask and other
+                        // render task size calculations. In future, we may consider creating multiple
+                        // render task trees, one per dirty region.
                         visibility_info.clipped_world_rect = rect;
+
+                        // If there is more than one dirty region, it's possible that this primitive
+                        // is inside the overal dirty rect, but doesn't intersect any of the individual
+                        // dirty rects. If that's the case, then we can skip drawing this primitive too.
+                        if dirty_region.dirty_rects.len() > 1 {
+                            let in_dirty_rects = dirty_region
+                                .dirty_rects
+                                .iter()
+                                .any(|dirty_rect| {
+                                    visibility_info.clipped_world_rect.intersects(&dirty_rect.world_rect)
+                                });
+
+                            if !in_dirty_rects {
+                                prim_instance.visibility_info = PrimitiveVisibilityIndex::INVALID;
+                                continue;
+                            }
+                        }
                     }
                     None => {
+                        // Outside the overall dirty rect, so can be skipped.
                         prim_instance.visibility_info = PrimitiveVisibilityIndex::INVALID;
                         continue;
                     }
                 }
             }
 
             let spatial_node = &frame_context
                 .clip_scroll_tree
@@ -2653,17 +2677,17 @@ impl PrimitiveStore {
                             common_data.prim_size,
                         );
                         let tight_clip_rect = prim_info
                             .combined_local_clip_rect
                             .intersection(&prim_rect).unwrap();
 
                         let visible_rect = compute_conservative_visible_rect(
                             prim_context,
-                            &pic_context.dirty_world_rect,
+                            &frame_state.current_dirty_region().combined.world_rect,
                             &tight_clip_rect
                         );
 
                         let base_edge_flags = edge_flags_for_tile_spacing(&image_data.tile_spacing);
 
                         let stride = image_data.stretch_size + image_data.tile_spacing;
 
                         let repetitions = ::image::repetitions(
@@ -2747,17 +2771,16 @@ impl PrimitiveStore {
 
                     *visible_tiles_range = decompose_repeated_primitive(
                         &prim_info.combined_local_clip_rect,
                         &prim_rect,
                         &prim_data.stretch_size,
                         &prim_data.tile_spacing,
                         prim_context,
                         frame_state,
-                        &pic_context.dirty_world_rect,
                         &mut scratch.gradient_tiles,
                         &mut |_, mut request| {
                             request.push([
                                 prim_data.start_point.x,
                                 prim_data.start_point.y,
                                 prim_data.end_point.x,
                                 prim_data.end_point.y,
                             ]);
@@ -2794,17 +2817,16 @@ impl PrimitiveStore {
 
                     *visible_tiles_range = decompose_repeated_primitive(
                         &prim_info.combined_local_clip_rect,
                         &prim_rect,
                         &prim_data.stretch_size,
                         &prim_data.tile_spacing,
                         prim_context,
                         frame_state,
-                        &pic_context.dirty_world_rect,
                         &mut scratch.gradient_tiles,
                         &mut |_, mut request| {
                             request.push([
                                 prim_data.center.x,
                                 prim_data.center.y,
                                 prim_data.params.start_radius,
                                 prim_data.params.end_radius,
                             ]);
@@ -2859,31 +2881,31 @@ fn write_segment<F>(
 
 fn decompose_repeated_primitive(
     combined_local_clip_rect: &LayoutRect,
     prim_local_rect: &LayoutRect,
     stretch_size: &LayoutSize,
     tile_spacing: &LayoutSize,
     prim_context: &PrimitiveContext,
     frame_state: &mut FrameBuildingState,
-    world_rect: &WorldRect,
     gradient_tiles: &mut GradientTileStorage,
     callback: &mut FnMut(&LayoutRect, GpuDataRequest),
 ) -> GradientTileRange {
     let mut visible_tiles = Vec::new();
+    let world_rect = frame_state.current_dirty_region().combined.world_rect;
 
     // Tighten the clip rect because decomposing the repeated image can
     // produce primitives that are partially covering the original image
     // rect and we want to clip these extra parts out.
     let tight_clip_rect = combined_local_clip_rect
         .intersection(prim_local_rect).unwrap();
 
     let visible_rect = compute_conservative_visible_rect(
         prim_context,
-        world_rect,
+        &world_rect,
         &tight_clip_rect
     );
     let stride = *stretch_size + *tile_spacing;
 
     let repetitions = ::image::repetitions(prim_local_rect, &visible_rect, stride);
     for Repetition { origin, .. } in repetitions {
         let mut handle = GpuCacheHandle::new();
         let rect = LayoutRect {
@@ -3281,37 +3303,40 @@ impl PrimitiveInstance {
                 pic_context.surface_index,
                 pic_state,
                 frame_context,
                 frame_state,
                 &mut data_stores.clip,
             );
             clip_mask_instances.push(clip_mask_kind);
         } else {
+            let dirty_world_rect = frame_state.current_dirty_region().combined.world_rect;
+
             for segment in segments {
                 // Build a clip chain for the smaller segment rect. This will
                 // often manage to eliminate most/all clips, and sometimes
                 // clip the segment completely.
+
                 let segment_clip_chain = frame_state
                     .clip_store
                     .build_clip_chain_instance(
                         self,
                         segment.local_rect.translate(&LayoutVector2D::new(
                             self.prim_origin.x,
                             self.prim_origin.y,
                         )),
                         self.local_clip_rect,
                         prim_context.spatial_node_index,
                         &pic_state.map_local_to_pic,
                         &pic_state.map_pic_to_world,
                         &frame_context.clip_scroll_tree,
                         frame_state.gpu_cache,
                         frame_state.resource_cache,
                         frame_context.device_pixel_scale,
-                        &pic_context.dirty_world_rect,
+                        &dirty_world_rect,
                         None,
                         &mut data_stores.clip,
                     );
 
                 let clip_mask_kind = segment.update_clip_task(
                     segment_clip_chain.as_ref(),
                     prim_info.clipped_world_rect,
                     root_spatial_node_index,
--- a/gfx/wr/webrender/src/renderer.rs
+++ b/gfx/wr/webrender/src/renderer.rs
@@ -3001,22 +3001,22 @@ impl Renderer {
         }
 
         self.profile_counters.vertices.add(6 * data.len());
     }
 
     fn handle_readback_composite(
         &mut self,
         draw_target: DrawTarget,
-        scissor_rect: Option<DeviceIntRect>,
+        uses_scissor: bool,
         source: &RenderTask,
         backdrop: &RenderTask,
         readback: &RenderTask,
     ) {
-        if scissor_rect.is_some() {
+        if uses_scissor {
             self.device.disable_scissor();
         }
 
         let cache_texture = self.texture_resolver
             .resolve(&TextureSource::PrevPassColor)
             .unwrap();
 
         // Before submitting the composite batch, do the
@@ -3061,17 +3061,17 @@ impl Renderer {
         self.device.bind_read_target(draw_target.into());
         self.device.blit_render_target(src, dest);
 
         // Restore draw target to current pass render target + layer, and reset
         // the read target.
         self.device.bind_draw_target(draw_target);
         self.device.reset_read_target();
 
-        if scissor_rect.is_some() {
+        if uses_scissor {
             self.device.enable_scissor();
         }
     }
 
     fn handle_blits(
         &mut self,
         blits: &[BlitJob],
         render_tasks: &RenderTaskTree,
@@ -3270,30 +3270,42 @@ impl Renderer {
                     &BatchTextures::no_texture(),
                     stats,
                 );
             }
         }
 
         self.handle_scaling(&target.scalings, TextureSource::PrevPassColor, projection, stats);
 
+        // Small helper fn to iterate a regions list, also invoking the closure
+        // if there are no regions.
+        fn iterate_regions<F>(
+            regions: &[DeviceIntRect],
+            mut f: F,
+        ) where F: FnMut(Option<DeviceIntRect>) {
+            if regions.is_empty() {
+                f(None)
+            } else {
+                for region in regions {
+                    f(Some(*region))
+                }
+            }
+        }
+
         for alpha_batch_container in &target.alpha_batch_containers {
-            if let Some(scissor_rect) = alpha_batch_container.scissor_rect {
-                // Note: `framebuffer_target_rect` needs a Y-flip before going to GL
-                let rect = if draw_target.is_default() {
-                    let mut rect = scissor_rect
-                        .intersection(&framebuffer_target_rect.to_i32())
-                        .unwrap_or(DeviceIntRect::zero());
-                    rect.origin.y = draw_target.dimensions().height as i32 - rect.origin.y - rect.size.height;
-                    rect
-                } else {
-                    scissor_rect
-                };
+            let uses_scissor = alpha_batch_container.task_scissor_rect.is_some() ||
+                               !alpha_batch_container.regions.is_empty();
+
+            if uses_scissor {
                 self.device.enable_scissor();
-                self.device.set_scissor_rect(rect);
+                let scissor_rect = draw_target.build_scissor_rect(
+                    alpha_batch_container.task_scissor_rect,
+                    framebuffer_target_rect,
+                );
+                self.device.set_scissor_rect(scissor_rect)
             }
 
             if !alpha_batch_container.opaque_batches.is_empty() {
                 let _gl = self.gpu_profile.start_marker("opaque batches");
                 let opaque_sampler = self.gpu_profile.start_sampler(GPU_SAMPLER_TAG_OPAQUE);
                 self.set_blend(false, framebuffer_kind);
                 //Note: depth equality is needed for split planes
                 self.device.set_depth_func(DepthFunction::LessEqual);
@@ -3310,21 +3322,35 @@ impl Renderer {
                     self.shaders.borrow_mut()
                         .get(&batch.key, self.debug_flags)
                         .bind(
                             &mut self.device, projection,
                             &mut self.renderer_errors,
                         );
 
                     let _timer = self.gpu_profile.start_timer(batch.key.kind.sampler_tag());
-                    self.draw_instanced_batch(
-                        &batch.instances,
-                        VertexArrayKind::Primitive,
-                        &batch.key.textures,
-                        stats
+
+                    iterate_regions(
+                        &alpha_batch_container.regions,
+                        |region| {
+                            if let Some(region) = region {
+                                let scissor_rect = draw_target.build_scissor_rect(
+                                    Some(region),
+                                    framebuffer_target_rect,
+                                );
+                                self.device.set_scissor_rect(scissor_rect);
+                            }
+
+                            self.draw_instanced_batch(
+                                &batch.instances,
+                                VertexArrayKind::Primitive,
+                                &batch.key.textures,
+                                stats
+                            );
+                        }
                     );
                 }
 
                 self.device.disable_depth_write();
                 self.gpu_profile.finish_sampler(opaque_sampler);
             }
 
             if !alpha_batch_container.alpha_batches.is_empty() {
@@ -3380,58 +3406,74 @@ impl Renderer {
 
                     // Handle special case readback for composites.
                     if let BatchKind::Brush(BrushBatchKind::MixBlend { task_id, source_id, backdrop_id }) = batch.key.kind {
                         // composites can't be grouped together because
                         // they may overlap and affect each other.
                         debug_assert_eq!(batch.instances.len(), 1);
                         self.handle_readback_composite(
                             draw_target,
-                            alpha_batch_container.scissor_rect,
+                            uses_scissor,
                             &render_tasks[source_id],
                             &render_tasks[task_id],
                             &render_tasks[backdrop_id],
                         );
                     }
 
                     let _timer = self.gpu_profile.start_timer(batch.key.kind.sampler_tag());
-                    self.draw_instanced_batch(
-                        &batch.instances,
-                        VertexArrayKind::Primitive,
-                        &batch.key.textures,
-                        stats
+
+                    iterate_regions(
+                        &alpha_batch_container.regions,
+                        |region| {
+                            if let Some(region) = region {
+                                let scissor_rect = draw_target.build_scissor_rect(
+                                    Some(region),
+                                    framebuffer_target_rect,
+                                );
+                                self.device.set_scissor_rect(scissor_rect);
+                            }
+
+                            self.draw_instanced_batch(
+                                &batch.instances,
+                                VertexArrayKind::Primitive,
+                                &batch.key.textures,
+                                stats
+                            );
+
+                            if batch.key.blend_mode == BlendMode::SubpixelWithBgColor {
+                                self.set_blend_mode_subpixel_with_bg_color_pass1(framebuffer_kind);
+                                self.device.switch_mode(ShaderColorMode::SubpixelWithBgColorPass1 as _);
+
+                                // When drawing the 2nd and 3rd passes, we know that the VAO, textures etc
+                                // are all set up from the previous draw_instanced_batch call,
+                                // so just issue a draw call here to avoid re-uploading the
+                                // instances and re-binding textures etc.
+                                self.device
+                                    .draw_indexed_triangles_instanced_u16(6, batch.instances.len() as i32);
+
+                                self.set_blend_mode_subpixel_with_bg_color_pass2(framebuffer_kind);
+                                self.device.switch_mode(ShaderColorMode::SubpixelWithBgColorPass2 as _);
+
+                                self.device
+                                    .draw_indexed_triangles_instanced_u16(6, batch.instances.len() as i32);
+                            }
+                        }
                     );
 
                     if batch.key.blend_mode == BlendMode::SubpixelWithBgColor {
-                        self.set_blend_mode_subpixel_with_bg_color_pass1(framebuffer_kind);
-                        self.device.switch_mode(ShaderColorMode::SubpixelWithBgColorPass1 as _);
-
-                        // When drawing the 2nd and 3rd passes, we know that the VAO, textures etc
-                        // are all set up from the previous draw_instanced_batch call,
-                        // so just issue a draw call here to avoid re-uploading the
-                        // instances and re-binding textures etc.
-                        self.device
-                            .draw_indexed_triangles_instanced_u16(6, batch.instances.len() as i32);
-
-                        self.set_blend_mode_subpixel_with_bg_color_pass2(framebuffer_kind);
-                        self.device.switch_mode(ShaderColorMode::SubpixelWithBgColorPass2 as _);
-
-                        self.device
-                            .draw_indexed_triangles_instanced_u16(6, batch.instances.len() as i32);
-
                         prev_blend_mode = BlendMode::None;
                     }
                 }
 
                 self.device.disable_depth();
                 self.set_blend(false, framebuffer_kind);
                 self.gpu_profile.finish_sampler(transparent_sampler);
             }
 
-            if alpha_batch_container.scissor_rect.is_some() {
+            if uses_scissor {
                 self.device.disable_scissor();
             }
 
             // At the end of rendering a container, blit across any cache tiles
             // to the texture cache for use on subsequent frames.
             if !alpha_batch_container.tile_blits.is_empty() {
                 let _timer = self.gpu_profile.start_timer(GPU_TAG_BLIT);
 
@@ -5334,8 +5376,9 @@ fn get_vao<'a>(vertex_array_kind: Vertex
     }
 }
 
 #[derive(Clone, Copy, PartialEq)]
 enum FramebufferKind {
     Main,
     Other,
 }
+
--- a/gfx/wr/webrender/src/tiling.rs
+++ b/gfx/wr/webrender/src/tiling.rs
@@ -376,17 +376,17 @@ impl RenderTarget for ColorRenderTarget 
         ctx: &mut RenderTargetContext,
         gpu_cache: &mut GpuCache,
         render_tasks: &mut RenderTaskTree,
         deferred_resolves: &mut Vec<DeferredResolve>,
         prim_headers: &mut PrimitiveHeaders,
         transforms: &mut TransformPalette,
         z_generator: &mut ZBufferIdGenerator,
     ) {
-        let mut merged_batches = AlphaBatchContainer::new(None);
+        let mut merged_batches = AlphaBatchContainer::new(None, Vec::new());
 
         for task_id in &self.alpha_tasks {
             let task = &render_tasks[*task_id];
 
             match task.clear_mode {
                 ClearMode::One |
                 ClearMode::Zero => {
                     panic!("bug: invalid clear mode for color task");
--- a/ipc/glue/BackgroundUtils.cpp
+++ b/ipc/glue/BackgroundUtils.cpp
@@ -110,25 +110,18 @@ already_AddRefed<nsIPrincipal> Principal
         if (NS_WARN_IF(NS_FAILED(rv))) {
           return nullptr;
         }
 
         rv = csp->SetRequestContext(nullptr, principal);
         if (NS_WARN_IF(NS_FAILED(rv))) {
           return nullptr;
         }
-
-        for (auto policy : info.securityPolicies()) {
-          rv = csp->AppendPolicy(policy.policy(), policy.reportOnlyFlag(),
-                                 policy.deliveredViaMetaTagFlag());
-          if (NS_WARN_IF(NS_FAILED(rv))) {
-            return nullptr;
-          }
-        }
-
+        static_cast<nsCSPContext*>(csp.get())->SetIPCPolicies(
+            info.securityPolicies());
         principal->SetCsp(csp);
       }
 
       return principal.forget();
     }
 
     case PrincipalInfo::TExpandedPrincipalInfo: {
       const ExpandedPrincipalInfo& info =
--- a/mobile/android/base/java/org/mozilla/gecko/LauncherActivity.java
+++ b/mobile/android/base/java/org/mozilla/gecko/LauncherActivity.java
@@ -11,43 +11,43 @@ import android.content.Intent;
 import android.net.Uri;
 import android.os.Bundle;
 import android.support.annotation.NonNull;
 import android.support.customtabs.CustomTabsIntent;
 import android.util.Log;
 
 import org.mozilla.gecko.home.HomeConfig;
 import org.mozilla.gecko.mma.MmaDelegate;
-import org.mozilla.gecko.switchboard.SwitchBoard;
-import org.mozilla.gecko.util.FileUtils;
 import org.mozilla.gecko.webapps.WebAppActivity;
 import org.mozilla.gecko.webapps.WebAppIndexer;
 import org.mozilla.gecko.customtabs.CustomTabsActivity;
 import org.mozilla.gecko.db.BrowserContract;
 import org.mozilla.gecko.mozglue.SafeIntent;
 import org.mozilla.gecko.preferences.GeckoPreferences;
 import org.mozilla.gecko.tabqueue.TabQueueHelper;
 import org.mozilla.gecko.tabqueue.TabQueueService;
 
 import static org.mozilla.gecko.BrowserApp.ACTIVITY_REQUEST_PREFERENCES;
 import static org.mozilla.gecko.deeplink.DeepLinkContract.DEEP_LINK_SCHEME;
 import static org.mozilla.gecko.deeplink.DeepLinkContract.LINK_BOOKMARK_LIST;
 import static org.mozilla.gecko.deeplink.DeepLinkContract.LINK_DEFAULT_BROWSER;
 import static org.mozilla.gecko.deeplink.DeepLinkContract.LINK_HISTORY_LIST;
+import static org.mozilla.gecko.deeplink.DeepLinkContract.LINK_OPEN;
 import static org.mozilla.gecko.deeplink.DeepLinkContract.LINK_PREFERENCES_HOME;
 import static org.mozilla.gecko.deeplink.DeepLinkContract.LINK_PREFERENCES;
 import static org.mozilla.gecko.deeplink.DeepLinkContract.LINK_PREFERENCES_ACCESSIBILITY;
 import static org.mozilla.gecko.deeplink.DeepLinkContract.LINK_PREFERENCES_GENERAL;
 import static org.mozilla.gecko.deeplink.DeepLinkContract.LINK_PREFERENCES_NOTIFICATIONS;
 import static org.mozilla.gecko.deeplink.DeepLinkContract.LINK_PREFERENCES_PRIAVACY;
 import static org.mozilla.gecko.deeplink.DeepLinkContract.LINK_PREFERENCES_SEARCH;
 import static org.mozilla.gecko.deeplink.DeepLinkContract.LINK_SAVE_AS_PDF;
 import static org.mozilla.gecko.deeplink.DeepLinkContract.LINK_SIGN_UP;
 import static org.mozilla.gecko.deeplink.DeepLinkContract.SUMO_DEFAULT_BROWSER;
 import static org.mozilla.gecko.deeplink.DeepLinkContract.LINK_FXA_SIGNIN;
+import static org.mozilla.gecko.deeplink.DeepLinkContract.URL_PARAM;
 import static org.mozilla.gecko.util.FileUtils.isContentUri;
 
 import org.mozilla.gecko.deeplink.DeepLinkContract;
 
 /**
  * Activity that receives incoming Intents and dispatches them to the appropriate activities (e.g. browser, custom tabs, web app).
  */
 public class LauncherActivity extends Activity {
@@ -191,24 +191,27 @@ public class LauncherActivity extends Ac
     }
 
     private void dispatchDeepLink(SafeIntent intent) {
         if (intent == null || intent.getData() == null || intent.getData().getHost() == null) {
             return;
         }
         final String deepLink = intent.getData().getHost();
         final String uid = intent.getData().getQueryParameter("uid");
-        final String localUid = getSharedPreferences(BrowserApp.class.getName(), MODE_PRIVATE).getString(MmaDelegate.KEY_ANDROID_PREF_STRING_LEANPLUM_DEVICE_ID, null);
+        final String localUid = MmaDelegate.getDeviceId(LauncherActivity.this);
         final boolean isMmaDeepLink = uid != null && localUid != null && uid.equals(localUid);
 
         if (!validateMmaDeepLink(deepLink, isMmaDeepLink)) {
             return;
         }
 
         switch (deepLink) {
+            case LINK_OPEN:
+                dispatchUrlDeepLink(intent);
+                break;
             case LINK_DEFAULT_BROWSER:
                 GeckoSharedPrefs.forApp(this).edit().putBoolean(GeckoPreferences.PREFS_DEFAULT_BROWSER, true).apply();
 
                 if (AppConstants.Versions.feature24Plus) {
                     // We are special casing the link to set the default browser here: On old Android versions we
                     // link to a SUMO page but on new Android versions we can link to the default app settings where
                     // the user can actually set a default browser (Bug 1312686).
                     final Intent changeDefaultApps = new Intent("android.settings.MANAGE_DEFAULT_APPS_SETTINGS");
@@ -299,9 +302,15 @@ public class LauncherActivity extends Ac
         }
 
         intent.setClassName(getApplicationContext(), AppConstants.MOZ_ANDROID_BROWSER_INTENT_CLASS);
 
         filterFlags(intent);
         startActivity(intent);
     }
 
+    private void dispatchUrlDeepLink(final SafeIntent intent) {
+        String url = intent.getData().getQueryParameter(URL_PARAM);
+        if (url != null) {
+            dispatchUrlIntent(url);
+        }
+    }
 }
--- a/mobile/android/base/java/org/mozilla/gecko/deeplink/DeepLinkContract.java
+++ b/mobile/android/base/java/org/mozilla/gecko/deeplink/DeepLinkContract.java
@@ -9,25 +9,28 @@ package org.mozilla.gecko.deeplink;
 public class DeepLinkContract {
 
     // Sumo page for setting Fennec as default browser
     public static final String SUMO_DEFAULT_BROWSER = "https://support.mozilla.org/kb/make-firefox-default-browser-android?utm_source=inproduct&amp;utm_medium=settings&amp;utm_campaign=mobileandroid";
     public static final String DEEP_LINK_SCHEME = "firefox";
 
     public static final String LINK_FXA_SIGNIN = "fxa-signin";
 
+    public static final String LINK_OPEN = "open";
     public static final String LINK_DEFAULT_BROWSER = "default_browser";
     public static final String LINK_SAVE_AS_PDF = "save_as_pdf";
     public static final String LINK_BOOKMARK_LIST = "bookmark_list";
     public static final String LINK_HISTORY_LIST = "history_list";
     public static final String LINK_SIGN_UP = "sign_up";
     public static final String LINK_PREFERENCES_GENERAL = "preferences_general";
     public static final String LINK_PREFERENCES = "preferences";
     public static final String LINK_PREFERENCES_PRIAVACY = "preferences_privacy";
     public static final String LINK_PREFERENCES_SEARCH = "preferences_search";
     public static final String LINK_PREFERENCES_NOTIFICATIONS = "preferences_notifications";
     public static final String LINK_PREFERENCES_ACCESSIBILITY = "preferences_accessibility";
     public static final String LINK_PREFERENCES_HOME = "preferences_home";
 
+    public static final String URL_PARAM = "url";
+
     public static final String ACCOUNTS_TOKEN_PARAM = "signin";
     public static final String ACCOUNTS_ENTRYPOINT_PARAM = "entrypoint";
     public static final String ACCOUNTS_UTM_PREFIX = "utm_";
 }
--- a/mobile/android/base/java/org/mozilla/gecko/mma/MmaDelegate.java
+++ b/mobile/android/base/java/org/mozilla/gecko/mma/MmaDelegate.java
@@ -7,17 +7,16 @@
 package org.mozilla.gecko.mma;
 
 import android.app.Activity;
 import android.content.Context;
 import android.content.SharedPreferences;
 import android.os.Bundle;
 import android.support.annotation.NonNull;
 import android.text.TextUtils;
-import android.util.Log;
 
 import org.mozilla.gecko.AppConstants;
 import org.mozilla.gecko.Experiments;
 import org.mozilla.gecko.MmaConstants;
 import org.mozilla.gecko.PrefsHelper;
 import org.mozilla.gecko.R;
 import org.mozilla.gecko.Tab;
 import org.mozilla.gecko.Tabs;
@@ -89,36 +88,35 @@ public class MmaDelegate {
                 registerInstalledPackagesReceiver(activity);
             }
         });
         applicationContext = activity.getApplicationContext();
         // Since user attributes are gathered in Fennec, not in MMA implementation,
         // we gather the information here then pass to mmaHelper.init()
         // Note that generateUserAttribute always return a non null HashMap.
         final Map<String, Object> attributes = gatherUserAttributes(activity);
-        final String deviceId = getDeviceId(activity);
+        String deviceId = getDeviceId(activity);
+        if (deviceId == null) {
+            deviceId = UUID.randomUUID().toString();
+            setDeviceId(activity, deviceId);
+        }
         mmaHelper.setDeviceId(deviceId);
         PrefsHelper.setPref(GeckoPreferences.PREFS_MMA_DEVICE_ID, deviceId);
         // above two config setup required to be invoked before mmaHelper.init.
         mmaHelper.init(activity, attributes);
 
         if (!isDefaultBrowser(activity)) {
             mmaHelper.event(MmaDelegate.LAUNCH_BUT_NOT_DEFAULT_BROWSER);
         }
         mmaHelper.event(MmaDelegate.LAUNCH_BROWSER);
 
         activityName = activity.getLocalClassName();
         notifyAboutPreviouslyInstalledPackages(activity);
 
-        ThreadUtils.postToUiThread(new Runnable() {
-            @Override
-            public void run() {
-                mmaHelper.listenOnceForVariableChanges(remoteVariablesListener);
-            }
-        });
+        ThreadUtils.postToUiThread(() -> mmaHelper.listenOnceForVariableChanges(remoteVariablesListener));
     }
 
     /**
      * Stop LeanPlum functionality.
      */
     public static void stop() {
         mmaHelper.stop();
     }
@@ -278,28 +276,28 @@ public class MmaDelegate {
             return false;
         }
     }
 
     public static PanelConfig getPanelConfig(@NonNull Context context, PanelConfig.TYPE panelConfigType, final boolean useLocalValues) {
         return mmaHelper.getPanelConfig(context, panelConfigType, useLocalValues);
     }
 
-    private static String getDeviceId(Activity activity) {
+    public static String getDeviceId(Activity activity) {
         if (SwitchBoard.isInExperiment(activity, Experiments.LEANPLUM_DEBUG)) {
             return DEBUG_LEANPLUM_DEVICE_ID;
         }
 
         final SharedPreferences prefs = activity.getPreferences(Context.MODE_PRIVATE);
-        String deviceId = prefs.getString(KEY_ANDROID_PREF_STRING_LEANPLUM_DEVICE_ID, null);
-        if (deviceId == null) {
-            deviceId = UUID.randomUUID().toString();
-            prefs.edit().putString(KEY_ANDROID_PREF_STRING_LEANPLUM_DEVICE_ID, deviceId).apply();
-        }
-        return deviceId;
+        return prefs.getString(KEY_ANDROID_PREF_STRING_LEANPLUM_DEVICE_ID, null);
+    }
+
+    private static void setDeviceId(Activity activity, String deviceId) {
+        final SharedPreferences prefs = activity.getPreferences(Context.MODE_PRIVATE);
+        prefs.edit().putString(KEY_ANDROID_PREF_STRING_LEANPLUM_DEVICE_ID, deviceId).apply();
     }
 
     private static void registerInstalledPackagesReceiver(@NonNull final Activity activity) {
         packageAddedReceiver = new PackageAddedReceiver();
         activity.registerReceiver(packageAddedReceiver, PackageAddedReceiver.getIntentFilter());
     }
 
     private static void unregisterInstalledPackagesReceiver(@NonNull final Activity activity) {
--- a/mobile/android/base/java/org/mozilla/gecko/notifications/NotificationReceiver.java
+++ b/mobile/android/base/java/org/mozilla/gecko/notifications/NotificationReceiver.java
@@ -84,22 +84,27 @@ public class NotificationReceiver extend
                                             final Uri data, final Intent intent) {
         final String name = data.getQueryParameter("name");
         final String cookie = data.getQueryParameter("cookie");
         final Intent persistentIntent = (Intent)
                 intent.getParcelableExtra(NotificationClient.PERSISTENT_INTENT_EXTRA);
 
         if (persistentIntent != null) {
             // Go through GeckoService for persistent notifications.
-            GeckoServicesCreatorService.enqueueWork(context, intent);
+            GeckoServicesCreatorService.enqueueWork(context, persistentIntent);
         }
 
         if (NotificationClient.CLICK_ACTION.equals(action)) {
             GeckoAppShell.onNotificationClick(name, cookie);
 
+            if (persistentIntent != null) {
+                // Don't launch GeckoApp if it's a background persistent notification.
+                return;
+            }
+
             final Intent appIntent = new Intent(GeckoApp.ACTION_ALERT_CALLBACK);
             appIntent.setComponent(new ComponentName(
                     data.getAuthority(), data.getPath().substring(1))); // exclude leading slash.
             appIntent.setData(data);
             appIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
             context.startActivity(appIntent);
 
         } else if (NotificationClient.CLOSE_ACTION.equals(action)) {
--- a/mobile/android/docs/mma.rst
+++ b/mobile/android/docs/mma.rst
@@ -172,16 +172,17 @@ List of current Events related data that
 {
   "event" : "E_Just_Installed_Klar"
 }
 
 Deep Links:
 Deep links are actions that can point Fennec to open certain pages or load features such as `show bookmark list` or
 `open a SUMO page`. When users see a prompt Leanplum message, they can click the button(s) on it. These buttons can
 trigger the following deep links
+* Link to open pages specifically in Fennec (firefox://open?url=)
 * Link to Set Default Browser settings (firefox://default_browser)
 * Link to specific Add-on page (http://link_to_the_add_on_page)
 * Link to sync signup/sign in (firefox://sign_up)
 * Link to default search engine settings (firefox://preferences_search)
 * Link to “Save as PDF” feature (firefox://save_as_pdf)
 * Take user directly to a Sign up for a newsletter (http://link_to_newsletter_page)
 * Link to bookmark list (firefox://bookmark_list)
 * Link to history list (firefox://history_list)