Merge mozilla-central to autoland. a=merge CLOSED TREE
authorshindli <shindli@mozilla.com>
Thu, 30 Aug 2018 14:22:17 +0300
changeset 491803 9a9cc00a4a7d5e964f70b10a2cd32e531da809a4
parent 491791 e5989730338493a5e91a41325080b99af9236bb6 (current diff)
parent 491802 815b46ed118286d682ce258e6147df5dc691eabf (diff)
child 491804 80282ed4858572c6cbe9e2de75d933c8ab2056e8
push id1815
push userffxbld-merge
push dateMon, 15 Oct 2018 10:40:45 +0000
treeherdermozilla-release@18d4c09e9378 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmerge
milestone63.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 mozilla-central to autoland. a=merge CLOSED TREE
browser/base/content/urlbarBindings.xml
--- a/browser/base/content/test/urlbar/browser_urlbarAddonIframe.js
+++ b/browser/base/content/test/urlbar/browser_urlbarAddonIframe.js
@@ -1,11 +1,9 @@
-/* Any copyright is dedicated to the Public Domain.
- * http://creativecommons.org/publicdomain/zero/1.0/ */
-
+/* eslint-disable mozilla/no-arbitrary-setTimeout */
 "use strict";
 
 // The purpose of this test is to test the urlbar popup's add-on iframe.  It has
 // a few parts:
 //
 // (1) This file, a normal browser mochitest.
 // (2) html/js files that are loaded in the urlbar popup's add-on iframe:
 //     urlbarAddonIframe.{html,js}
@@ -130,24 +128,23 @@ add_task(async function() {
   // urlbar.getPanelHeight
   let height = iframe.getBoundingClientRect().height;
   let readHeight = await promiseUrlbarFunctionCall("getPanelHeight");
   Assert.equal(readHeight, height, "getPanelHeight");
 
   // urlbar.setPanelHeight
   let newHeight = height + 100;
   await promiseUrlbarFunctionCall("setPanelHeight", newHeight);
-  // The height change is animated, so give it time to complete.
-  await TestUtils.waitForCondition(
-    () => Math.round(iframe.getBoundingClientRect().height) == newHeight,
-    "Wait for panel height change after setPanelHeight"
-  ).catch(ex => {
-    info("Last detected height: " + Math.round(iframe.getBoundingClientRect().height));
-    throw ex;
+  await new Promise(resolve => {
+    // The height change is animated, so give it time to complete.  Again, wait
+    // a sec to be safe.
+    setTimeout(resolve, 1000);
   });
+  Assert.equal(iframe.getBoundingClientRect().height, newHeight,
+               "setPanelHeight");
 });
 
 function promiseIframeLoad() {
   let msgName = "TestIframeLoadAck";
   return new Promise(resolve => {
     info("Waiting for iframe load ack");
     gMsgMan.addMessageListener(msgName, function onMsg(msg) {
       info("Received iframe load ack");
--- a/browser/base/content/urlbarBindings.xml
+++ b/browser/base/content/urlbarBindings.xml
@@ -88,17 +88,16 @@ file, You can obtain one at http://mozil
         this.inputField.controllers.insertControllerAt(0, this._copyCutController);
         this.inputField.addEventListener("paste", this);
         this.inputField.addEventListener("mousedown", this);
         this.inputField.addEventListener("mousemove", this);
         this.inputField.addEventListener("mouseout", this);
         this.inputField.addEventListener("overflow", this);
         this.inputField.addEventListener("underflow", this);
         this.inputField.addEventListener("scrollend", this);
-        window.addEventListener("resize", this);
 
         var textBox = document.getAnonymousElementByAttribute(this,
                                                 "anonid", "moz-input-box");
         // Force the Custom Element to upgrade until Bug 1470242 handles this:
         customElements.upgrade(textBox);
         var cxmenu = textBox.menupopup;
         var pasteAndGo;
         cxmenu.addEventListener("popupshowing", function() {
@@ -154,17 +153,16 @@ file, You can obtain one at http://mozil
         this.inputField.controllers.removeController(this._copyCutController);
         this.inputField.removeEventListener("paste", this);
         this.inputField.removeEventListener("mousedown", this);
         this.inputField.removeEventListener("mousemove", this);
         this.inputField.removeEventListener("mouseout", this);
         this.inputField.removeEventListener("overflow", this);
         this.inputField.removeEventListener("underflow", this);
         this.inputField.removeEventListener("scrollend", this);
-        window.removeEventListener("resize", this);
 
         if (this._deferredKeyEventTimeout) {
           clearTimeout(this._deferredKeyEventTimeout);
           this._deferredKeyEventTimeout = null;
         }
 
         // Null out the one-offs' popup and textbox so that it cleans up its
         // internal state for both.  Most importantly, it removes the event
@@ -508,51 +506,36 @@ file, You can obtain one at http://mozil
         <body><![CDATA[
           // This method must not modify the given URL such that calling
           // nsIURIFixup::createFixupURI with the result will produce a different URI.
           return this._mayTrimURLs ? trimURL(aURL) : aURL;
         ]]></body>
       </method>
 
       <field name="_formattingEnabled">true</field>
-
-      <!--
-        If the input value is a URL, the input is not focused, and formatting is
-        enabled, this method highlights the domain, and if mixed content is
-        present, it crosses out the https scheme.  It also ensures that the host
-        is visible (not scrolled out of sight).  Otherwise it removes formatting.
-
-        @param  onlyEnsureFormattedHostVisible
-                Pass true to skip formatting and instead only ensure that the
-                host is visible.
-      -->
       <method name="formatValue">
-        <parameter name="onlyEnsureFormattedHostVisible"/>
         <body><![CDATA[
           // Used to avoid re-entrance in async callbacks.
           let instance = this._formattingInstance = {};
 
           if (!this.editor)
             return;
 
+          // Cleanup previously set styles.
+          this.scheme.value = "";
           let controller, strikeOut, selection;
-
-          if (!onlyEnsureFormattedHostVisible) {
-            // Cleanup previously set styles.
-            this.scheme.value = "";
-            if (this._formattingEnabled) {
-              controller = this.editor.selectionController;
-              strikeOut = controller.getSelection(controller.SELECTION_URLSTRIKEOUT);
-              strikeOut.removeAllRanges();
-              selection = controller.getSelection(controller.SELECTION_URLSECONDARY);
-              selection.removeAllRanges();
-              this.formatScheme(controller.SELECTION_URLSTRIKEOUT, true);
-              this.formatScheme(controller.SELECTION_URLSECONDARY, true);
-              this.inputField.style.setProperty("--urlbar-scheme-size", "0px");
-            }
+          if (this._formattingEnabled) {
+            controller = this.editor.selectionController;
+            strikeOut = controller.getSelection(controller.SELECTION_URLSTRIKEOUT);
+            strikeOut.removeAllRanges();
+            selection = controller.getSelection(controller.SELECTION_URLSECONDARY);
+            selection.removeAllRanges();
+            this.formatScheme(controller.SELECTION_URLSTRIKEOUT, true);
+            this.formatScheme(controller.SELECTION_URLSECONDARY, true);
+            this.inputField.style.setProperty("--urlbar-scheme-size", "0px");
           }
 
           let textNode = this.editor.rootElement.firstChild;
           let value = textNode.textContent;
           if (!value)
             return;
 
           if (this.focused)
@@ -611,17 +594,17 @@ file, You can obtain one at http://mozil
             // char just after the domain, and in such a case we should not
             // scroll to the left.
             if (directionality == window.windowUtils.DIRECTION_RTL &&
                 value[preDomain.length + domain.length] != "\u200E") {
               this.inputField.scrollLeft = this.inputField.scrollLeftMax;
             }
           });
 
-          if (onlyEnsureFormattedHostVisible || !this._formattingEnabled)
+          if (!this._formattingEnabled)
             return;
 
           this.formatScheme(controller.SELECTION_URLSECONDARY);
 
           // Strike out the "https" part if mixed active content is loaded.
           if (this.getAttribute("pageproxystate") == "valid" &&
               value.startsWith("https:") &&
               gBrowser.securityUI.state &
@@ -1451,39 +1434,16 @@ file, You can obtain one at http://mozil
               this.updateTextOverflow();
               break;
             case "TabSelect":
               // The autocomplete controller uses heuristic on some internal caches
               // to handle cases like backspace, autofill or repeated searches.
               // Ensure to clear those internal caches when switching tabs.
               this.controller.resetInternalState();
               break;
-            case "resize":
-              // Make sure the host remains visible in the input field when the
-              // window is resized.  We don't want to hurt resize performance,
-              // though, so do this only after resize events have stopped and a
-              // small timeout has elapsed.
-              if (aEvent.target == window) {
-                if (this._resizeThrottleTimeout) {
-                  clearTimeout(this._resizeThrottleTimeout);
-                }
-                this._resizeThrottleTimeout = setTimeout(() => {
-                  this._resizeThrottleTimeout = null;
-
-                  // Close the popup since it would be wrongly sized, we'll
-                  // recalculate a proper size on reopening. For example, this
-                  // may happen when using special OS resize functions like
-                  // Win+Arrow.
-                  this.closePopup();
-
-                  // Make the host visible via formatValue.
-                  this.formatValue(true);
-                }, 100);
-              }
-              break;
           }
         ]]></body>
       </method>
 
       <method name="updateTextOverflow">
         <body><![CDATA[
           if (this._inOverflow) {
             window.promiseDocumentFlushed(() => {
--- a/docshell/shistory/nsISHTransaction.idl
+++ b/docshell/shistory/nsISHTransaction.idl
@@ -1,45 +1,24 @@
-/* -*- Mode: IDL; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*-
- *
- * This Source Code Form is subject to the terms of the Mozilla Public
+/* -*- Mode: IDL; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 #include "nsISupports.idl"
 
 interface nsISHEntry;
 
-/**
- * The nsISHTransaction.
- */
-
-
 [scriptable, uuid(2EDF705F-D252-4971-9F09-71DD0F760DC6)]
 interface nsISHTransaction : nsISupports
 {
-	/**
-     * The nsISHEntry for the current transaction
-     */
-	attribute nsISHEntry sHEntry;
-
-	/**
-	 * The parent of this transaction
-	 */
-	attribute nsISHTransaction prev;
+  /**
+   * The nsISHEntry for the current transaction.
+   */
+  attribute nsISHEntry sHEntry;
 
-	/**
-	 * The legitimate child of this transaction 
-	 */
-	attribute nsISHTransaction next;
-
-	/**
-	 *	Specifies if this transaction should persist.  If not it will be replaced
-	 * by new additions to the list.
-	 */
-	attribute boolean persist;
-
-	/**
-	 * Create a transaction with parent and History Entry 
-	 */
-	 void create(in nsISHEntry aSHEntry, in nsISHTransaction aPrev);
+  /**
+   * Specifies if this transaction should persist. If not it will be replaced
+   * by new additions to the list.
+   */
+  attribute boolean persist;
 };
 
--- a/docshell/shistory/nsISHistoryInternal.idl
+++ b/docshell/shistory/nsISHistoryInternal.idl
@@ -26,19 +26,19 @@ interface nsISHistoryInternal: nsISuppor
    * @param aEntry - The entry to add
    * @param aPersist - If true this specifies that the entry should persist
    * in the list.  If false, this means that when new entries are added
    * this element will not appear in the session history list.
    */
    void  addEntry(in nsISHEntry aEntry, in boolean aPersist);
 
   /**
-   * Get the root transaction
+   * Get the transaction at a particular index.
    */
-   readonly attribute nsISHTransaction rootTransaction;
+  nsISHTransaction GetTransactionAtIndex(in int32_t aIndex);
 
   /**
    * Sets the toplevel docshell object to which this SHistory object belongs to.
    */
    void setRootDocShell(in nsIDocShell rootDocShell);
 
   /**
    * Update the index maintained by sessionHistory
--- a/docshell/shistory/nsSHTransaction.cpp
+++ b/docshell/shistory/nsSHTransaction.cpp
@@ -3,99 +3,49 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 #include "nsSHTransaction.h"
 #include "nsISHEntry.h"
 
 nsSHTransaction::nsSHTransaction()
-  : mPrev(nullptr)
-  , mPersist(true)
+  : mPersist(true)
 {
 }
 
 nsSHTransaction::~nsSHTransaction()
 {
 }
 
 NS_IMPL_ADDREF(nsSHTransaction)
 NS_IMPL_RELEASE(nsSHTransaction)
 
 NS_INTERFACE_MAP_BEGIN(nsSHTransaction)
   NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsISHTransaction)
   NS_INTERFACE_MAP_ENTRY(nsISHTransaction)
 NS_INTERFACE_MAP_END
 
 NS_IMETHODIMP
-nsSHTransaction::Create(nsISHEntry* aSHEntry, nsISHTransaction* aPrev)
-{
-  SetSHEntry(aSHEntry);
-  if (aPrev) {
-    aPrev->SetNext(this);
-  }
-
-  SetPrev(aPrev);
-  return NS_OK;
-}
-
-NS_IMETHODIMP
 nsSHTransaction::GetSHEntry(nsISHEntry** aResult)
 {
   NS_ENSURE_ARG_POINTER(aResult);
   *aResult = mSHEntry;
   NS_IF_ADDREF(*aResult);
   return NS_OK;
 }
 
 NS_IMETHODIMP
 nsSHTransaction::SetSHEntry(nsISHEntry* aSHEntry)
 {
   mSHEntry = aSHEntry;
   return NS_OK;
 }
 
 NS_IMETHODIMP
-nsSHTransaction::GetNext(nsISHTransaction** aResult)
-{
-  NS_ENSURE_ARG_POINTER(aResult);
-  *aResult = mNext;
-  NS_IF_ADDREF(*aResult);
-  return NS_OK;
-}
-
-NS_IMETHODIMP
-nsSHTransaction::SetNext(nsISHTransaction* aNext)
-{
-  if (aNext) {
-    NS_ENSURE_SUCCESS(aNext->SetPrev(this), NS_ERROR_FAILURE);
-  }
-
-  mNext = aNext;
-  return NS_OK;
-}
-
-NS_IMETHODIMP
-nsSHTransaction::SetPrev(nsISHTransaction* aPrev)
-{
-  /* This is weak reference to parent. Do not Addref it */
-  mPrev = aPrev;
-  return NS_OK;
-}
-
-nsresult
-nsSHTransaction::GetPrev(nsISHTransaction** aResult)
-{
-  NS_ENSURE_ARG_POINTER(aResult);
-  *aResult = mPrev;
-  NS_IF_ADDREF(*aResult);
-  return NS_OK;
-}
-
-NS_IMETHODIMP
 nsSHTransaction::SetPersist(bool aPersist)
 {
   mPersist = aPersist;
   return NS_OK;
 }
 
 NS_IMETHODIMP
 nsSHTransaction::GetPersist(bool* aPersist)
--- a/docshell/shistory/nsSHTransaction.h
+++ b/docshell/shistory/nsSHTransaction.h
@@ -19,15 +19,13 @@ public:
   NS_DECL_NSISHTRANSACTION
 
   nsSHTransaction();
 
 protected:
   virtual ~nsSHTransaction();
 
 protected:
-  nsISHTransaction* mPrev;  // Weak Reference
-  nsCOMPtr<nsISHTransaction> mNext;
   nsCOMPtr<nsISHEntry> mSHEntry;
   bool mPersist;
 };
 
 #endif /* nsSHTransaction_h */
--- a/docshell/shistory/nsSHistory.cpp
+++ b/docshell/shistory/nsSHistory.cpp
@@ -233,17 +233,16 @@ nsSHistory::EvictContentViewerForTransac
   GetIndexOfEntry(entry, &index);
   if (index != -1) {
     RemoveDynEntries(index, entry);
   }
 }
 
 nsSHistory::nsSHistory()
   : mIndex(-1)
-  , mLength(0)
   , mRequestedIndex(-1)
   , mRootDocShell(nullptr)
 {
   // Add this new SHistory object to the list
   gSHistoryList.insertBack(this);
 }
 
 nsSHistory::~nsSHistory()
@@ -626,77 +625,67 @@ nsSHistory::AddEntry(nsISHEntry* aSHEntr
   // If we have a root docshell, update the docshell id of the root shentry to
   // match the id of that docshell
   if (mRootDocShell) {
     nsID docshellID = mRootDocShell->HistoryID();
     aSHEntry->SetDocshellID(&docshellID);
   }
 
   nsCOMPtr<nsISHTransaction> currentTxn;
-
-  if (mListRoot) {
-    GetTransactionAtIndex(mIndex, getter_AddRefs(currentTxn));
-  }
+  GetTransactionAtIndex(mIndex, getter_AddRefs(currentTxn));
 
   bool currentPersist = true;
   if (currentTxn) {
     currentTxn->GetPersist(&currentPersist);
   }
 
   int32_t currentIndex = mIndex;
 
   if (!currentPersist) {
     NOTIFY_LISTENERS(OnHistoryReplaceEntry, (currentIndex));
     NS_ENSURE_SUCCESS(currentTxn->SetSHEntry(aSHEntry), NS_ERROR_FAILURE);
     currentTxn->SetPersist(aPersist);
     return NS_OK;
   }
 
-  nsCOMPtr<nsISHTransaction> txn = new nsSHTransaction();
   nsCOMPtr<nsIURI> uri;
   aSHEntry->GetURI(getter_AddRefs(uri));
   NOTIFY_LISTENERS(OnHistoryNewEntry, (uri, currentIndex));
 
-  // If a listener has changed mIndex, we need to get currentTxn again,
-  // otherwise we'll be left at an inconsistent state (see bug 320742)
-  if (currentIndex != mIndex) {
-    GetTransactionAtIndex(mIndex, getter_AddRefs(currentTxn));
-  }
+  // Note that a listener may have changed mIndex. So use mIndex instead of
+  // currentIndex.
+
+  nsCOMPtr<nsISHTransaction> txn = new nsSHTransaction();
+  txn->SetPersist(aPersist);
+  txn->SetSHEntry(aSHEntry);
 
-  // Set the ShEntry and parent for the transaction. setting the
-  // parent will properly set the parent child relationship
-  txn->SetPersist(aPersist);
-  NS_ENSURE_SUCCESS(txn->Create(aSHEntry, currentTxn), NS_ERROR_FAILURE);
+  // Remove all transactions after the current one, add the new one, and set
+  // the new one as the current one.
+  MOZ_ASSERT(mIndex >= -1);
+  mTransactions.TruncateLength(mIndex + 1);
+  mTransactions.AppendElement(txn);
+  mIndex++;
 
-  // A little tricky math here...  Basically when adding an object regardless of
-  // what the length was before, it should always be set back to the current and
-  // lop off the forward.
-  mLength = (++mIndex + 1);
-  NOTIFY_LISTENERS(OnLengthChanged, (mLength));
+  NOTIFY_LISTENERS(OnLengthChanged, (Length()));
   NOTIFY_LISTENERS(OnIndexChanged, (mIndex));
 
-  // If this is the very first transaction, initialize the list
-  if (!mListRoot) {
-    mListRoot = txn;
-  }
-
   // Purge History list if it is too long
-  if (gHistoryMaxSize >= 0 && mLength > gHistoryMaxSize) {
-    PurgeHistory(mLength - gHistoryMaxSize);
+  if (gHistoryMaxSize >= 0 && Length() > gHistoryMaxSize) {
+    PurgeHistory(Length() - gHistoryMaxSize);
   }
 
   return NS_OK;
 }
 
 /* Get size of the history list */
 NS_IMETHODIMP
 nsSHistory::GetCount(int32_t* aResult)
 {
   NS_ENSURE_ARG_POINTER(aResult);
-  *aResult = mLength;
+  *aResult = Length();
   return NS_OK;
 }
 
 /* Get index of the history list */
 NS_IMETHODIMP
 nsSHistory::GetIndex(int32_t* aResult)
 {
   MOZ_ASSERT(aResult, "null out param?");
@@ -736,123 +725,59 @@ nsSHistory::GetEntryAtIndex(int32_t aInd
   }
   return rv;
 }
 
 /* Get the transaction at a given index */
 nsresult
 nsSHistory::GetTransactionAtIndex(int32_t aIndex, nsISHTransaction** aResult)
 {
-  nsresult rv;
   NS_ENSURE_ARG_POINTER(aResult);
 
-  if (mLength <= 0 || aIndex < 0 || aIndex >= mLength) {
-    return NS_ERROR_FAILURE;
-  }
-
-  if (!mListRoot) {
+  if (aIndex < 0 || aIndex >= Length()) {
     return NS_ERROR_FAILURE;
   }
 
-  if (aIndex == 0) {
-    *aResult = mListRoot;
-    NS_ADDREF(*aResult);
-    return NS_OK;
-  }
-
-  int32_t cnt = 0;
-  nsCOMPtr<nsISHTransaction> tempPtr;
-  rv = GetRootTransaction(getter_AddRefs(tempPtr));
-  if (NS_FAILED(rv) || !tempPtr) {
-    return NS_ERROR_FAILURE;
-  }
-
-  while (true) {
-    nsCOMPtr<nsISHTransaction> ptr;
-    rv = tempPtr->GetNext(getter_AddRefs(ptr));
-    if (NS_SUCCEEDED(rv) && ptr) {
-      cnt++;
-      if (cnt == aIndex) {
-        ptr.forget(aResult);
-        break;
-      } else {
-        tempPtr = ptr;
-        continue;
-      }
-    } else {
-      return NS_ERROR_FAILURE;
-    }
-  }
-
+  *aResult = mTransactions[aIndex];
+  NS_ADDREF(*aResult);
   return NS_OK;
 }
 
 /* Get the index of a given entry */
 NS_IMETHODIMP
 nsSHistory::GetIndexOfEntry(nsISHEntry* aSHEntry, int32_t* aResult)
 {
   NS_ENSURE_ARG(aSHEntry);
   NS_ENSURE_ARG_POINTER(aResult);
   *aResult = -1;
 
-  if (mLength <= 0) {
-    return NS_ERROR_FAILURE;
-  }
-
-  nsCOMPtr<nsISHTransaction> currentTxn;
-  int32_t cnt = 0;
-
-  nsresult rv = GetRootTransaction(getter_AddRefs(currentTxn));
-  if (NS_FAILED(rv) || !currentTxn) {
-    return NS_ERROR_FAILURE;
-  }
-
-  while (true) {
+  for (int32_t i = 0; i < Length(); i++) {
     nsCOMPtr<nsISHEntry> entry;
-    rv = currentTxn->GetSHEntry(getter_AddRefs(entry));
+    nsresult rv = mTransactions[i]->GetSHEntry(getter_AddRefs(entry));
     if (NS_FAILED(rv) || !entry) {
       return NS_ERROR_FAILURE;
     }
 
     if (aSHEntry == entry) {
-      *aResult = cnt;
-      break;
+      *aResult = i;
+      return NS_OK;
     }
-
-    rv = currentTxn->GetNext(getter_AddRefs(currentTxn));
-    if (NS_FAILED(rv) || !currentTxn) {
-      return NS_ERROR_FAILURE;
-    }
-
-    cnt++;
   }
 
-  return NS_OK;
+  return NS_ERROR_FAILURE;
 }
 
 #ifdef DEBUG
 nsresult
 nsSHistory::PrintHistory()
 {
-  nsCOMPtr<nsISHTransaction> txn;
-  int32_t index = 0;
-  nsresult rv;
-
-  if (!mListRoot) {
-    return NS_ERROR_FAILURE;
-  }
-
-  txn = mListRoot;
-
-  while (1) {
-    if (!txn) {
-      break;
-    }
+  for (int32_t i = 0; i < Length(); i++) {
+    nsCOMPtr<nsISHTransaction> txn = mTransactions[i];
     nsCOMPtr<nsISHEntry> entry;
-    rv = txn->GetSHEntry(getter_AddRefs(entry));
+    nsresult rv = txn->GetSHEntry(getter_AddRefs(entry));
     if (NS_FAILED(rv) && !entry) {
       return NS_ERROR_FAILURE;
     }
 
     nsCOMPtr<nsILayoutHistoryState> layoutHistoryState;
     nsCOMPtr<nsIURI> uri;
     nsString title;
 
@@ -861,47 +786,28 @@ nsSHistory::PrintHistory()
     entry->GetTitle(getter_Copies(title));
 
 #if 0
     nsAutoCString url;
     if (uri) {
       uri->GetSpec(url);
     }
 
-    printf("**** SH Transaction #%d, Entry = %x\n", index, entry.get());
+    printf("**** SH Transaction #%d, Entry = %x\n", i, entry.get());
     printf("\t\t URL = %s\n", url.get());
 
     printf("\t\t Title = %s\n", NS_LossyConvertUTF16toASCII(title).get());
     printf("\t\t layout History Data = %x\n", layoutHistoryState.get());
 #endif
-
-    nsCOMPtr<nsISHTransaction> next;
-    rv = txn->GetNext(getter_AddRefs(next));
-    if (NS_SUCCEEDED(rv) && next) {
-      txn = next;
-      index++;
-      continue;
-    } else {
-      break;
-    }
   }
 
   return NS_OK;
 }
 #endif
 
-NS_IMETHODIMP
-nsSHistory::GetRootTransaction(nsISHTransaction** aResult)
-{
-  NS_ENSURE_ARG_POINTER(aResult);
-  *aResult = mListRoot;
-  NS_IF_ADDREF(*aResult);
-  return NS_OK;
-}
-
 /* Get the max size of the history list */
 NS_IMETHODIMP
 nsSHistory::GetMaxLength(int32_t* aResult)
 {
   NS_ENSURE_ARG_POINTER(aResult);
   *aResult = gHistoryMaxSize;
   return NS_OK;
 }
@@ -910,67 +816,64 @@ nsSHistory::GetMaxLength(int32_t* aResul
 NS_IMETHODIMP
 nsSHistory::SetMaxLength(int32_t aMaxSize)
 {
   if (aMaxSize < 0) {
     return NS_ERROR_ILLEGAL_VALUE;
   }
 
   gHistoryMaxSize = aMaxSize;
-  if (mLength > aMaxSize) {
-    PurgeHistory(mLength - aMaxSize);
+  if (Length() > aMaxSize) {
+    PurgeHistory(Length() - aMaxSize);
   }
   return NS_OK;
 }
 
+void
+nsSHistory::WindowIndices(int32_t aIndex, int32_t* aOutStartIndex,
+                          int32_t* aOutEndIndex)
+{
+  *aOutStartIndex = std::max(0, aIndex - nsISHistory::VIEWER_WINDOW);
+  *aOutEndIndex = std::min(Length() - 1, aIndex + nsISHistory::VIEWER_WINDOW);
+}
+
 NS_IMETHODIMP
-nsSHistory::PurgeHistory(int32_t aEntries)
+nsSHistory::PurgeHistory(int32_t aNumEntries)
 {
-  if (mLength <= 0 || aEntries <= 0) {
+  if (Length() <= 0 || aNumEntries <= 0) {
     return NS_ERROR_FAILURE;
   }
 
-  aEntries = std::min(aEntries, mLength);
+  aNumEntries = std::min(aNumEntries, Length());
 
   bool purgeHistory = true;
   NOTIFY_LISTENERS_CANCELABLE(OnHistoryPurge, purgeHistory,
-                              (aEntries, &purgeHistory));
+                              (aNumEntries, &purgeHistory));
 
   if (!purgeHistory) {
     // Listener asked us not to purge
     return NS_SUCCESS_LOSS_OF_INSIGNIFICANT_DATA;
   }
 
-  int32_t cnt = 0;
-  while (cnt < aEntries) {
-    nsCOMPtr<nsISHTransaction> nextTxn;
-    if (mListRoot) {
-      mListRoot->GetNext(getter_AddRefs(nextTxn));
-      mListRoot->SetNext(nullptr);
-    }
-    mListRoot = nextTxn;
-    if (mListRoot) {
-      mListRoot->SetPrev(nullptr);
-    }
-    cnt++;
-  }
-  mLength -= cnt;
-  mIndex -= cnt;
+  // Remove the first `aNumEntries` entries.
+  mTransactions.RemoveElementsAt(0, aNumEntries);
+
+  mIndex -= aNumEntries;
 
   // Now if we were not at the end of the history, mIndex could have
   // become far too negative.  If so, just set it to -1.
   if (mIndex < -1) {
     mIndex = -1;
   }
 
-  NOTIFY_LISTENERS(OnLengthChanged, (mLength));
+  NOTIFY_LISTENERS(OnLengthChanged, (Length()));
   NOTIFY_LISTENERS(OnIndexChanged, (mIndex))
 
   if (mRootDocShell) {
-    mRootDocShell->HistoryPurged(cnt);
+    mRootDocShell->HistoryPurged(aNumEntries);
   }
 
   return NS_OK;
 }
 
 NS_IMETHODIMP
 nsSHistory::AddSHistoryListener(nsISHistoryListener* aListener)
 {
@@ -1003,21 +906,16 @@ nsSHistory::RemoveSHistoryListener(nsISH
  */
 NS_IMETHODIMP
 nsSHistory::ReplaceEntry(int32_t aIndex, nsISHEntry* aReplaceEntry)
 {
   NS_ENSURE_ARG(aReplaceEntry);
   nsresult rv;
   nsCOMPtr<nsISHTransaction> currentTxn;
 
-  if (!mListRoot) {
-    // Session History is not initialised.
-    return NS_ERROR_FAILURE;
-  }
-
   rv = GetTransactionAtIndex(aIndex, getter_AddRefs(currentTxn));
 
   if (currentTxn) {
     nsCOMPtr<nsISHistory> shistoryOfEntry;
     aReplaceEntry->GetSHistory(getter_AddRefs(shistoryOfEntry));
     if (shistoryOfEntry && shistoryOfEntry != this) {
       NS_WARNING("The entry has been associated to another nsISHistory instance. "
                  "Try nsISHEntry.clone() and nsISHEntry.abandonBFCacheEntry() "
@@ -1055,22 +953,18 @@ nsSHistory::EvictOutOfRangeContentViewer
   return NS_OK;
 }
 
 NS_IMETHODIMP
 nsSHistory::EvictAllContentViewers()
 {
   // XXXbz we don't actually do a good job of evicting things as we should, so
   // we might have viewers quite far from mIndex.  So just evict everything.
-  nsCOMPtr<nsISHTransaction> trans = mListRoot;
-  while (trans) {
-    EvictContentViewerForTransaction(trans);
-
-    nsCOMPtr<nsISHTransaction> temp = trans;
-    temp->GetNext(getter_AddRefs(trans));
+  for (int32_t i = 0; i < Length(); i++) {
+    EvictContentViewerForTransaction(mTransactions[i]);
   }
 
   return NS_OK;
 }
 
 NS_IMETHODIMP
 nsSHistory::GetCanGoBack(bool* aCanGoBack)
 {
@@ -1217,49 +1111,45 @@ nsSHistory::EvictOutOfRangeWindowContent
   // (Note that it's entirely legal to have two copies of one content viewer
   // separated by a different content viewer -- call pushState twice, go back
   // once, and refresh -- so we can't rely on identical viewers only appearing
   // adjacent to one another.)
 
   if (aIndex < 0) {
     return;
   }
-  NS_ENSURE_TRUE_VOID(aIndex < mLength);
+  NS_ENSURE_TRUE_VOID(aIndex < Length());
 
   // Calculate the range that's safe from eviction.
-  int32_t startSafeIndex = std::max(0, aIndex - nsISHistory::VIEWER_WINDOW);
-  int32_t endSafeIndex = std::min(mLength, aIndex + nsISHistory::VIEWER_WINDOW);
+  int32_t startSafeIndex, endSafeIndex;
+  WindowIndices(aIndex, &startSafeIndex, &endSafeIndex);
 
   LOG(("EvictOutOfRangeWindowContentViewers(index=%d), "
-       "mLength=%d. Safe range [%d, %d]",
-       aIndex, mLength, startSafeIndex, endSafeIndex));
+       "Length()=%d. Safe range [%d, %d]",
+       aIndex, Length(), startSafeIndex, endSafeIndex));
 
   // The content viewers in range aIndex -/+ VIEWER_WINDOW will not be
   // evicted.  Collect a set of them so we don't accidentally evict one of them
   // if it appears outside this range.
   nsCOMArray<nsIContentViewer> safeViewers;
-  nsCOMPtr<nsISHTransaction> trans;
-  GetTransactionAtIndex(startSafeIndex, getter_AddRefs(trans));
-  for (int32_t i = startSafeIndex; trans && i <= endSafeIndex; i++) {
-    nsCOMPtr<nsIContentViewer> viewer = GetContentViewerForTransaction(trans);
+  for (int32_t i = startSafeIndex; i <= endSafeIndex; i++) {
+    nsCOMPtr<nsIContentViewer> viewer =
+      GetContentViewerForTransaction(mTransactions[i]);
     safeViewers.AppendObject(viewer);
-    nsCOMPtr<nsISHTransaction> temp = trans;
-    temp->GetNext(getter_AddRefs(trans));
   }
 
   // Walk the SHistory list and evict any content viewers that aren't safe.
-  GetTransactionAtIndex(0, getter_AddRefs(trans));
-  while (trans) {
+  // (It's important that the condition checks Length(), rather than a cached
+  // copy of Length(), because the length might change between iterations.)
+  for (int32_t i = 0; i < Length(); i++) {
+    nsCOMPtr<nsISHTransaction> trans = mTransactions[i];
     nsCOMPtr<nsIContentViewer> viewer = GetContentViewerForTransaction(trans);
     if (safeViewers.IndexOf(viewer) == -1) {
       EvictContentViewerForTransaction(trans);
     }
-
-    nsCOMPtr<nsISHTransaction> temp = trans;
-    temp->GetNext(getter_AddRefs(trans));
   }
 }
 
 namespace {
 
 class TransactionAndDistance
 {
 public:
@@ -1331,22 +1221,20 @@ nsSHistory::GloballyEvictContentViewers(
     //   * history.pushState or hash navigations, in which case a copy of the
     //     content viewer should exist within the range, or
     //
     //   * bugs which cause us not to call nsSHistory::EvictContentViewers()
     //     often enough.  Once we do call EvictContentViewers() for the
     //     SHistory object in question, we'll do a full search of its history
     //     and evict the out-of-range content viewers, so we don't bother here.
     //
-    int32_t startIndex = std::max(0, shist->mIndex - nsISHistory::VIEWER_WINDOW);
-    int32_t endIndex = std::min(shist->mLength - 1,
-                                shist->mIndex + nsISHistory::VIEWER_WINDOW);
-    nsCOMPtr<nsISHTransaction> trans;
-    shist->GetTransactionAtIndex(startIndex, getter_AddRefs(trans));
-    for (int32_t i = startIndex; trans && i <= endIndex; i++) {
+    int32_t startIndex, endIndex;
+    shist->WindowIndices(shist->mIndex, &startIndex, &endIndex);
+    for (int32_t i = startIndex; i <= endIndex; i++) {
+      nsCOMPtr<nsISHTransaction> trans = shist->mTransactions[i];
       nsCOMPtr<nsIContentViewer> contentViewer =
         GetContentViewerForTransaction(trans);
 
       if (contentViewer) {
         // Because one content viewer might belong to multiple SHEntries, we
         // have to search through shTransactions to see if we already know
         // about this content viewer.  If we find the viewer, update its
         // distance from the SHistory's index and continue.
@@ -1364,19 +1252,16 @@ nsSHistory::GloballyEvictContentViewers(
         // If we didn't find a TransactionAndDistance for this content viewer,
         // make a new one.
         if (!found) {
           TransactionAndDistance container(shist, trans,
                                            DeprecatedAbs(i - shist->mIndex));
           shTransactions.AppendElement(container);
         }
       }
-
-      nsCOMPtr<nsISHTransaction> temp = trans;
-      temp->GetNext(getter_AddRefs(trans));
     }
 
     // We've found all the transactions belonging to shist which have viewers.
     // Add those transactions to our global list and move on.
     transactions.AppendElements(shTransactions);
   }
 
   // We now have collected all cached content viewers.  First check that we
@@ -1401,41 +1286,32 @@ nsSHistory::GloballyEvictContentViewers(
 nsresult
 nsSHistory::FindTransactionForBFCache(nsIBFCacheEntry* aEntry,
                                       nsISHTransaction** aResult,
                                       int32_t* aResultIndex)
 {
   *aResult = nullptr;
   *aResultIndex = -1;
 
-  int32_t startIndex = std::max(0, mIndex - nsISHistory::VIEWER_WINDOW);
-  int32_t endIndex = std::min(mLength - 1, mIndex + nsISHistory::VIEWER_WINDOW);
-  nsCOMPtr<nsISHTransaction> trans;
-  GetTransactionAtIndex(startIndex, getter_AddRefs(trans));
+  int32_t startIndex, endIndex;
+  WindowIndices(mIndex, &startIndex, &endIndex);
 
-  int32_t i;
-  for (i = startIndex; trans && i <= endIndex; ++i) {
+  for (int32_t i = startIndex; i <= endIndex; ++i) {
+    nsCOMPtr<nsISHTransaction> trans = mTransactions[i];
     nsCOMPtr<nsISHEntry> entry;
     trans->GetSHEntry(getter_AddRefs(entry));
 
     // Does entry have the same BFCacheEntry as the argument to this method?
     if (entry->HasBFCacheEntry(aEntry)) {
-      break;
+      trans.forget(aResult);
+      *aResultIndex = i;
+      return NS_OK;
     }
-
-    nsCOMPtr<nsISHTransaction> temp = trans;
-    temp->GetNext(getter_AddRefs(trans));
   }
-  if (i > endIndex) {
-    return NS_ERROR_FAILURE;
-  }
-
-  trans.forget(aResult);
-  *aResultIndex = i;
-  return NS_OK;
+  return NS_ERROR_FAILURE;
 }
 
 nsresult
 nsSHistory::EvictExpiredContentViewerForEntry(nsIBFCacheEntry* aEntry)
 {
   int32_t index;
   nsCOMPtr<nsISHTransaction> trans;
   FindTransactionForBFCache(aEntry, getter_AddRefs(trans), &index);
@@ -1585,48 +1461,29 @@ IsSameTree(nsISHEntry* aEntry1, nsISHEnt
 
 bool
 nsSHistory::RemoveDuplicate(int32_t aIndex, bool aKeepNext)
 {
   NS_ASSERTION(aIndex >= 0, "aIndex must be >= 0!");
   NS_ASSERTION(aIndex != 0 || aKeepNext,
                "If we're removing index 0 we must be keeping the next");
   NS_ASSERTION(aIndex != mIndex, "Shouldn't remove mIndex!");
+
   int32_t compareIndex = aKeepNext ? aIndex + 1 : aIndex - 1;
+
+  nsresult rv;
   nsCOMPtr<nsISHEntry> root1, root2;
-  GetEntryAtIndex(aIndex, false, getter_AddRefs(root1));
-  GetEntryAtIndex(compareIndex, false, getter_AddRefs(root2));
+  rv = GetEntryAtIndex(aIndex, false, getter_AddRefs(root1));
+  NS_ENSURE_SUCCESS(rv, false);
+  rv = GetEntryAtIndex(compareIndex, false, getter_AddRefs(root2));
+  NS_ENSURE_SUCCESS(rv, false);
+
   if (IsSameTree(root1, root2)) {
-    nsCOMPtr<nsISHTransaction> txToRemove, txToKeep, txNext, txPrev;
-    GetTransactionAtIndex(aIndex, getter_AddRefs(txToRemove));
-    GetTransactionAtIndex(compareIndex, getter_AddRefs(txToKeep));
-    if (!txToRemove) {
-      return false;
-    }
-    NS_ENSURE_TRUE(txToKeep, false);
-    txToRemove->GetNext(getter_AddRefs(txNext));
-    txToRemove->GetPrev(getter_AddRefs(txPrev));
-    txToRemove->SetNext(nullptr);
-    txToRemove->SetPrev(nullptr);
-    if (aKeepNext) {
-      if (txPrev) {
-        txPrev->SetNext(txToKeep);
-      } else {
-        txToKeep->SetPrev(nullptr);
-      }
-    } else {
-      txToKeep->SetNext(txNext);
-    }
+    mTransactions.RemoveElementAt(aIndex);
 
-    if (aIndex == 0 && aKeepNext) {
-      NS_ASSERTION(txToRemove == mListRoot,
-                   "Transaction at index 0 should be mListRoot!");
-      // We're removing the very first session history transaction!
-      mListRoot = txToKeep;
-    }
     if (mRootDocShell) {
       static_cast<nsDocShell*>(mRootDocShell)->HistoryTransactionRemoved(aIndex);
     }
 
     // Adjust our indices to reflect the removed transaction
     if (mIndex > aIndex) {
       mIndex = mIndex - 1;
       NOTIFY_LISTENERS(OnIndexChanged, (mIndex));
@@ -1642,18 +1499,17 @@ nsSHistory::RemoveDuplicate(int32_t aInd
 
     // NB: We don't need to guard on mRequestedIndex being nonzero here,
     // because either they're strictly greater than aIndex which is at least
     // zero, or they are equal to aIndex in which case aKeepNext must be true
     // if aIndex is zero.
     if (mRequestedIndex > aIndex || (mRequestedIndex == aIndex && !aKeepNext)) {
       mRequestedIndex = mRequestedIndex - 1;
     }
-    --mLength;
-    NOTIFY_LISTENERS(OnLengthChanged, (mLength));
+    NOTIFY_LISTENERS(OnLengthChanged, (Length()));
     return true;
   }
   return false;
 }
 
 NS_IMETHODIMP_(void)
 nsSHistory::RemoveEntries(nsTArray<nsID>& aIDs, int32_t aStartIndex)
 {
@@ -1823,17 +1679,17 @@ nsSHistory::LoadEntry(int32_t aIndex, lo
 {
   if (!mRootDocShell) {
     return NS_ERROR_FAILURE;
   }
 
   nsCOMPtr<nsIURI> nextURI;
   nsCOMPtr<nsISHEntry> prevEntry;
   nsCOMPtr<nsISHEntry> nextEntry;
-  if (aIndex < 0 || aIndex >= mLength) {
+  if (aIndex < 0 || aIndex >= Length()) {
     // The index is out of range
     return NS_ERROR_FAILURE;
   }
 
   // This is a normal local history navigation.
   // Keep note of requested history index in mRequestedIndex.
   mRequestedIndex = aIndex;
 
--- a/docshell/shistory/nsSHistory.h
+++ b/docshell/shistory/nsSHistory.h
@@ -129,17 +129,16 @@ public:
                                      nsDocShell* aRootShell,
                                      WalkHistoryEntriesFunc aCallback,
                                      void* aData);
 
 private:
   virtual ~nsSHistory();
   friend class nsSHistoryObserver;
 
-  nsresult GetTransactionAtIndex(int32_t aIndex, nsISHTransaction** aResult);
   nsresult LoadDifferingEntries(nsISHEntry* aPrevEntry, nsISHEntry* aNextEntry,
                                 nsIDocShell* aRootDocShell, long aLoadType,
                                 bool& aDifferenceFound);
   nsresult InitiateLoad(nsISHEntry* aFrameEntry, nsIDocShell* aFrameDS,
                         long aLoadType);
 
   nsresult LoadEntry(int32_t aIndex, long aLoadType, uint32_t aHistCmd);
 
@@ -170,20 +169,25 @@ private:
   // aIndex is the index of the transaction which may be removed.
   // If aKeepNext is true, aIndex is compared to aIndex + 1,
   // otherwise comparison is done to aIndex - 1.
   bool RemoveDuplicate(int32_t aIndex, bool aKeepNext);
 
   // Track all bfcache entries and evict on expiration.
   mozilla::UniquePtr<HistoryTracker> mHistoryTracker;
 
-  nsCOMPtr<nsISHTransaction> mListRoot;
-  int32_t mIndex;
-  int32_t mLength;
-  int32_t mRequestedIndex;
+  nsTArray<nsCOMPtr<nsISHTransaction>> mTransactions;
+  int32_t mIndex;           // -1 means "no index"
+  int32_t mRequestedIndex;  // -1 means "no requested index"
+
+  void WindowIndices(int32_t aIndex, int32_t* aOutStartIndex,
+                     int32_t* aOutEndIndex);
+
+  // Length of mTransactions.
+  int32_t Length() { return int32_t(mTransactions.Length()); }
 
   // Session History listeners
   nsAutoTObserverArray<nsWeakPtr, 2> mListeners;
 
   // Weak reference. Do not refcount this.
   nsIDocShell* mRootDocShell;
 
   // Max viewers allowed total, across all SHistory objects
--- a/toolkit/components/extensions/ExtensionXPCShellUtils.jsm
+++ b/toolkit/components/extensions/ExtensionXPCShellUtils.jsm
@@ -425,26 +425,21 @@ class ExtensionWrapper {
 
   onMessage(msg, callback) {
     this.checkDuplicateListeners(msg);
     this.messageHandler.set(msg, callback);
   }
 }
 
 class AOMExtensionWrapper extends ExtensionWrapper {
-  constructor(testScope, xpiFile, installType) {
+  constructor(testScope) {
     super(testScope);
 
     this.onEvent = this.onEvent.bind(this);
 
-    this.file = xpiFile;
-    this.installType = installType;
-
-    this.cleanupFiles = [xpiFile];
-
     Management.on("ready", this.onEvent);
     Management.on("shutdown", this.onEvent);
     Management.on("startup", this.onEvent);
 
     AddonTestUtils.on("addon-manager-shutdown", this.onEvent);
     AddonTestUtils.on("addon-manager-started", this.onEvent);
 
     AddonManager.addAddonListener(this);
@@ -457,33 +452,16 @@ class AOMExtensionWrapper extends Extens
     Management.off("ready", this.onEvent);
     Management.off("shutdown", this.onEvent);
     Management.off("startup", this.onEvent);
 
     AddonTestUtils.off("addon-manager-shutdown", this.onEvent);
     AddonTestUtils.off("addon-manager-started", this.onEvent);
 
     AddonManager.removeAddonListener(this);
-
-    for (let file of this.cleanupFiles.splice(0)) {
-      try {
-        Services.obs.notifyObservers(file, "flush-cache-entry");
-        file.remove(false);
-      } catch (e) {
-        Cu.reportError(e);
-      }
-    }
-  }
-
-  maybeSetID(uri, id) {
-    if (!this.id && uri instanceof Ci.nsIJARURI &&
-        uri.JARFile.QueryInterface(Ci.nsIFileURL)
-           .file.equals(this.file)) {
-      this.id = id;
-    }
   }
 
   setRestarting() {
     if (this.state !== "restarting") {
       this.startupPromise = new Promise(resolve => {
         this.resolveStartup = resolve;
       }).then(async result => {
         await this.addonPromise;
@@ -556,16 +534,79 @@ class AOMExtensionWrapper extends Extens
           this.state = "running";
           this.resolveStartup(extension);
         }
         break;
       }
     }
   }
 
+  async _flushCache() {
+    if (this.extension && this.extension.rootURI instanceof Ci.nsIJARURI) {
+      let file = this.extension.rootURI.JARFile.QueryInterface(Ci.nsIFileURL).file;
+      await Services.ppmm.broadcastAsyncMessage("Extension:FlushJarCache", {path: file.path});
+    }
+  }
+
+  get version() {
+    return this.addon && this.addon.version;
+  }
+
+  async unload() {
+    await this._flushCache();
+    return super.unload();
+  }
+
+  async upgrade(data) {
+    this.startupPromise = new Promise(resolve => {
+      this.resolveStartup = resolve;
+    });
+    this.state = "restarting";
+
+    await this._flushCache();
+
+    let xpiFile = Extension.generateXPI(data);
+
+    this.cleanupFiles.push(xpiFile);
+
+    return this._install(xpiFile);
+  }
+}
+
+class InstallableWrapper extends AOMExtensionWrapper {
+  constructor(testScope, xpiFile, installType) {
+    super(testScope);
+
+    this.file = xpiFile;
+    this.installType = installType;
+
+    this.cleanupFiles = [xpiFile];
+  }
+
+  destroy() {
+    super.destroy();
+
+    for (let file of this.cleanupFiles.splice(0)) {
+      try {
+        Services.obs.notifyObservers(file, "flush-cache-entry");
+        file.remove(false);
+      } catch (e) {
+        Cu.reportError(e);
+      }
+    }
+  }
+
+  maybeSetID(uri, id) {
+    if (!this.id && uri instanceof Ci.nsIJARURI &&
+        uri.JARFile.QueryInterface(Ci.nsIFileURL)
+           .file.equals(this.file)) {
+      this.id = id;
+    }
+  }
+
   _install(xpiFile) {
     if (this.installType === "temporary") {
       return AddonManager.installTemporaryAddon(xpiFile).then(addon => {
         this.id = addon.id;
         this.addon = addon;
 
         return this.startupPromise;
       }).catch(e => {
@@ -588,59 +629,43 @@ class AOMExtensionWrapper extends Extens
         install.addListener(listener);
         install.install();
 
         return this.startupPromise;
       });
     }
   }
 
-  async _flushCache() {
-    if (this.extension && this.extension.rootURI instanceof Ci.nsIJARURI) {
-      let file = this.extension.rootURI.JARFile.QueryInterface(Ci.nsIFileURL).file;
-      await Services.ppmm.broadcastAsyncMessage("Extension:FlushJarCache", {path: file.path});
-    }
-  }
-
-  get version() {
-    return this.addon && this.addon.version;
-  }
-
   startup() {
     if (this.state != "uninitialized") {
       throw new Error("Extension already started");
     }
 
     this.state = "pending";
     this.startupPromise = new Promise(resolve => {
       this.resolveStartup = resolve;
     });
 
     return this._install(this.file);
   }
+}
 
-  async unload() {
-    await this._flushCache();
-    return super.unload();
-  }
+class ExternallyInstalledWrapper extends AOMExtensionWrapper {
+  constructor(testScope, id) {
+    super(testScope);
 
-  async upgrade(data) {
+    this.id = id;
     this.startupPromise = new Promise(resolve => {
       this.resolveStartup = resolve;
     });
-    this.state = "restarting";
-
-    await this._flushCache();
 
-    let xpiFile = Extension.generateXPI(data);
+    this.state = "restarting";
+  }
 
-    this.cleanupFiles.push(xpiFile);
-
-    return this._install(xpiFile);
-  }
+  maybeSetID(uri, id) { }
 }
 
 var ExtensionTestUtils = {
   BASE_MANIFEST,
 
   async normalizeManifest(manifest, manifestType = "manifest.WebExtensionManifest",
                           baseManifest = BASE_MANIFEST) {
     await Management.lazyInit();
@@ -749,17 +774,23 @@ var ExtensionTestUtils = {
     }
 
     let extension = Extension.generate(data);
 
     return new ExtensionWrapper(this.currentScope, extension);
   },
 
   loadExtensionXPI(xpiFile, useAddonManager = "temporary") {
-    return new AOMExtensionWrapper(this.currentScope, xpiFile, useAddonManager);
+    return new InstallableWrapper(this.currentScope, xpiFile, useAddonManager);
+  },
+
+  // Create a wrapper for a webextension that will be installed
+  // by some external process (e.g., Normandy)
+  expectExtension(id) {
+    return new ExternallyInstalledWrapper(this.currentScope, id);
   },
 
   get remoteContentScripts() {
     return REMOTE_CONTENT_SCRIPTS;
   },
 
   set remoteContentScripts(val) {
     REMOTE_CONTENT_SCRIPTS = !!val;
--- a/toolkit/components/normandy/lib/AddonStudies.jsm
+++ b/toolkit/components/normandy/lib/AddonStudies.jsm
@@ -240,10 +240,49 @@ var AddonStudies = {
     await getStore(db).put(study);
 
     Services.obs.notifyObservers(study, STUDY_ENDED_TOPIC, `${study.recipeId}`);
     TelemetryEvents.sendEvent("unenroll", "addon_study", study.name, {
       addonId: study.addonId,
       addonVersion: study.addonVersion,
       reason,
     });
+
+    await this.onUnenroll(study.addonId);
+  },
+
+  // Maps extension id -> Set(callbacks)
+  _unenrollListeners: new Map(),
+
+  /**
+   * Register a callback to be invoked when a given study ends.
+   *
+   * @param {string} id         The extension id
+   * @param {function} listener The callback
+   */
+  addUnenrollListener(id, listener) {
+    let listeners = this._unenrollListeners.get(id);
+    if (!listeners) {
+      listeners = new Set();
+      this._unenrollListeners.set(id, listeners);
+    }
+    listeners.add(listener);
+  },
+
+  /**
+   * Invoke the unenroll callback (if any) for the given extension
+   *
+   * @param {string} id The extension id
+   *
+   * @returns {Promise} A Promise resolved after the unenroll listener
+   *                    (if any) has finished its unenroll tasks.
+   */
+  onUnenroll(id) {
+    let callbacks = this._unenrollListeners.get(id);
+    let promises = [];
+    if (callbacks) {
+      for (let callback of callbacks) {
+        promises.push(callback());
+      }
+    }
+    return Promise.all(promises);
   },
 };
new file mode 100644
--- /dev/null
+++ b/toolkit/components/normandy/test/unit/test_addon_unenroll.js
@@ -0,0 +1,135 @@
+
+ChromeUtils.import("resource://testing-common/AddonTestUtils.jsm");
+ChromeUtils.import("resource://testing-common/ExtensionXPCShellUtils.jsm");
+ChromeUtils.import("resource://normandy/actions/AddonStudyAction.jsm");
+ChromeUtils.import("resource://normandy/lib/TelemetryEvents.jsm");
+ChromeUtils.import("resource://gre/modules/AddonManager.jsm");
+
+const global = this;
+
+add_task(async function test_addon_unenroll() {
+  ExtensionTestUtils.init(global);
+  AddonTestUtils.init(global);
+  AddonTestUtils.createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "1.9.2");
+  AddonTestUtils.overrideCertDB();
+  await AddonTestUtils.promiseStartupManager();
+
+  TelemetryEvents.init();
+
+  const ID = "study@tests.mozilla.org";
+
+  // Create a test extension that uses webextension experiments to install
+  // an unenroll listener.
+  let xpi = AddonTestUtils.createTempWebExtensionFile({
+    manifest: {
+      applications: {
+        gecko: { id: ID },
+      },
+
+      experiment_apis: {
+        study: {
+          schema: "schema.json",
+          parent: {
+            scopes: ["addon_parent"],
+            script: "api.js",
+            paths: [["study"]],
+          },
+        }
+      },
+    },
+
+    files: {
+      "schema.json": JSON.stringify([
+        {
+          namespace: "study",
+          events: [
+            {
+              name: "onStudyEnded",
+              type: "function",
+            },
+          ],
+        }
+      ]),
+
+      // The code below is serialized into a file embedded in an extension.
+      // But by including it here as code, eslint can analyze it.  However,
+      // this code runs in a different environment with different globals,
+      // the following line avoids false eslint warnings:
+      /* globals browser, ExtensionAPI */
+      "api.js": () => {
+        const {AddonStudies} = ChromeUtils.import("resource://normandy/lib/AddonStudies.jsm", {});
+        const {ExtensionCommon} = ChromeUtils.import("resource://gre/modules/ExtensionCommon.jsm", {});
+        this.study = class extends ExtensionAPI {
+          getAPI(context) {
+            return {
+              study: {
+                onStudyEnded: new ExtensionCommon.EventManager({
+                  context,
+                  name: "study.onStudyEnded",
+                  register: fire => {
+                    AddonStudies.addUnenrollListener(this.extension.id,
+                                                     () => fire.sync());
+                    return () => {};
+                  },
+                }).api(),
+              }
+            };
+          }
+        };
+      },
+    },
+
+    background() {
+      browser.study.onStudyEnded.addListener(() => {
+        browser.test.sendMessage("got-event");
+        return new Promise(resolve => {
+          browser.test.onMessage.addListener(resolve);
+        });
+      });
+    }
+  });
+
+  const server = AddonTestUtils.createHttpServer({hosts: ["example.com"]});
+  server.registerFile("/study.xpi", xpi);
+
+  // Begin by telling Normandy to install the test extension above
+  // that uses a webextension experiment to register a blocking callback
+  // to be invoked when the study ends.
+  let extension = ExtensionTestUtils.expectExtension(ID);
+
+  const RECIPE_ID = 1;
+  let action = new AddonStudyAction();
+  await action.runRecipe({
+    id: RECIPE_ID,
+    type: "addon-study",
+    arguments: {
+      name: "addon unenroll test",
+      description: "testing",
+      addonUrl: "http://example.com/study.xpi",
+    },
+  });
+
+  await extension.awaitStartup();
+
+  let addon = await AddonManager.getAddonByID(ID);
+  ok(addon, "Extension is installed");
+
+  // Tell Normandy to end the study, the extension event should be fired.
+  let unenrollPromise = action.unenroll(RECIPE_ID);
+
+  await extension.awaitMessage("got-event");
+  info("Got onStudyEnded event in extension");
+
+  // The extension has not yet finished its unenrollment tasks, so it
+  // should not yet be uninstalled.
+  addon = await AddonManager.getAddonByID(ID);
+  ok(addon, "Extension has not yet been uninstalled");
+
+  // Once the extension does resolve the promise returned from the
+  // event listener, the uninstall can proceed.
+  extension.sendMessage("resolve");
+  await unenrollPromise;
+
+  addon = await AddonManager.getAddonByID(ID);
+  equal(addon, null, "Afer resolving studyEnded promise, extension is uninstalled");
+});
--- a/toolkit/components/normandy/test/unit/xpcshell.ini
+++ b/toolkit/components/normandy/test/unit/xpcshell.ini
@@ -1,12 +1,14 @@
 [DEFAULT]
 head = head_xpc.js
+firefox-appdir = browser
 support-files =
   mock_api/**
   invalid_recipe_signature_api/**
   query_server.sjs
   echo_server.sjs
   utils.js
 tags = normandy
 
+[test_addon_unenroll.js]
 [test_NormandyApi.js]
 [test_SandboxManager.js]
--- a/toolkit/modules/sessionstore/SessionHistory.jsm
+++ b/toolkit/modules/sessionstore/SessionHistory.jsm
@@ -76,20 +76,23 @@ var SessionHistoryInternal = {
 
     let data = {entries: [], userContextId: loadContext.originAttributes.userContextId };
     // We want to keep track how many entries we *could* have collected and
     // how many we skipped, so we can sanitiy-check the current history index
     // and also determine whether we need to get any fallback data or not.
     let skippedCount = 0, entryCount = 0;
 
     if (history && history.count > 0) {
-      // Loop over the transaction linked list directly so we can get the
-      // persist property for each transaction.
-      for (let txn = history.legacySHistory.QueryInterface(Ci.nsISHistoryInternal).rootTransaction;
-           txn; entryCount++, txn = txn.next) {
+      // Loop over the transactions so we can get the persist property for each
+      // one.
+      let shistory = history.legacySHistory.QueryInterface(Ci.nsISHistory);
+      let shistoryInternal = history.legacySHistory.QueryInterface(Ci.nsISHistoryInternal);
+      let count = shistory.count;
+      for ( ; entryCount < count; entryCount++) {
+        let txn = shistoryInternal.GetTransactionAtIndex(entryCount);
         if (entryCount <= aFromIdx) {
           skippedCount++;
           continue;
         }
         let entry = this.serializeEntry(txn.sHEntry);
         entry.persist = txn.persist;
         data.entries.push(entry);
       }