Merge inbound to mozilla-central. a=merge
authorshindli <shindli@mozilla.com>
Thu, 30 Aug 2018 12:55:47 +0300
changeset 489170 09de350001f0
parent 489150 d8fc943845d6 (current diff)
parent 489169 fe26ccb3ec3b (diff)
child 489171 815b46ed1182
push id9738
push useraciure@mozilla.com
push dateMon, 03 Sep 2018 16:13:51 +0000
treeherdermozilla-beta@f5841408a66c [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmerge
milestone63.0a1
first release with
nightly linux32
09de350001f0 / 63.0a1 / 20180830100138 / files
nightly mac
09de350001f0 / 63.0a1 / 20180830100138 / files
nightly linux64
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
releases
nightly linux32
nightly mac
Merge inbound to mozilla-central. a=merge
layout/painting/FrameLayerBuilder.cpp
layout/painting/nsDisplayList.cpp
--- 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);
       }