Bug 1276553 - Part 1: Make nsISHistory support partial mode. r=smaug
authorSamael Wang <freesamael@gmail.com>
Fri, 14 Oct 2016 15:18:29 +0800
changeset 319713 09b9bd35cf63d6b26fcbbcacee6043c8e82e38d8
parent 319712 1ba26e18f7cabe9f232780081d8b66366c7ddef4
child 319714 97ff05c0dc7d5ec0efb4078cc6e288e6f8ba3e90
push id20748
push userphilringnalda@gmail.com
push dateFri, 28 Oct 2016 03:39:55 +0000
treeherderfx-team@715360440695 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerssmaug
bugs1276553
milestone52.0a1
Bug 1276553 - Part 1: Make nsISHistory support partial mode. r=smaug Make nsISHistory support partial mode and create interfaces which DOM level grouped session history should implement. MozReview-Commit-ID: BXhBY6aJ0f7
docshell/shistory/moz.build
docshell/shistory/nsIGroupedSHistory.idl
docshell/shistory/nsIPartialSHistory.idl
docshell/shistory/nsIPartialSHistoryListener.idl
docshell/shistory/nsISHistory.idl
docshell/shistory/nsISHistoryListener.idl
docshell/shistory/nsSHistory.cpp
docshell/shistory/nsSHistory.h
--- a/docshell/shistory/moz.build
+++ b/docshell/shistory/moz.build
@@ -1,16 +1,19 @@
 # -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
 # vim: set filetype=python:
 # This Source Code Form is subject to the terms of the Mozilla Public
 # License, v. 2.0. If a copy of the MPL was not distributed with this
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
 XPIDL_SOURCES += [
     'nsIBFCacheEntry.idl',
+    'nsIGroupedSHistory.idl',
+    'nsIPartialSHistory.idl',
+    'nsIPartialSHistoryListener.idl',
     'nsISHContainer.idl',
     'nsISHEntry.idl',
     'nsISHistory.idl',
     'nsISHistoryInternal.idl',
     'nsISHistoryListener.idl',
     'nsISHTransaction.idl',
 ]
 
new file mode 100644
--- /dev/null
+++ b/docshell/shistory/nsIGroupedSHistory.idl
@@ -0,0 +1,47 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/* 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 nsIFrameLoader;
+interface nsIPartialSHistory;
+
+/**
+ * nsIGroupedSHistory represent a combined session history across multiple
+ * root docshells (usually browser tabs). The participating nsISHistory can
+ * either be in chrome process or in content process, but nsIGroupedSHistory
+ * itself lives in chrome process. The communication is proxyed through
+ * nsIPartialSHistory.
+ */
+[scriptable, builtinclass, uuid(813e498d-73a8-449a-be09-6187e62c5352)]
+interface nsIGroupedSHistory : nsISupports
+{
+  // The total number of entries of all its partial session histories.
+  [infallible] readonly attribute unsigned long count;
+
+  /**
+   * Remove all partial histories after currently active one (if any) and then
+   * append the given partial session history to the end of the list.
+   */
+  void appendPartialSessionHistory(in nsIPartialSHistory aPartialHistory);
+
+  /**
+   * Notify the grouped session history that the active partial session history
+   * has been modified. All partial session histories after the active one
+   * will be removed and destroy.
+   */
+  void onPartialSessionHistoryChange(in nsIPartialSHistory aPartialHistory);
+
+  /**
+   * Find the proper partial session history and navigate to the entry
+   * corresponding to the given global index. Note it doesn't swap frameloaders,
+   * but rather return the target loader for the caller to swap.
+   *
+   * @param aGlobalIndex        The global index to navigate to.
+   * @param aTargetLoaderToSwap The owner frameloader of the to-be-navigate
+   *                            partial session history.
+   */
+  void gotoIndex(in unsigned long aGlobalIndex, out nsIFrameLoader aTargetLoaderToSwap);
+};
new file mode 100644
--- /dev/null
+++ b/docshell/shistory/nsIPartialSHistory.idl
@@ -0,0 +1,62 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/* 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 nsIFrameLoader;
+
+/**
+ * nsIPartialSHistory represents a part of nsIGroupedSHistory. It associates to
+ * a "partial" nsISHistory in either local or remote process.
+ */
+[scriptable, builtinclass, uuid(5cd75e28-838c-4a0a-972e-6005f736ef7a)]
+interface nsIPartialSHistory : nsISupports
+{
+  // The number of entries of its corresponding nsISHistory.
+  [infallible] readonly attribute unsigned long count;
+
+  // If it's part of a grouped session history, globalIndexOffset denotes the
+  // number of entries ahead.
+  [infallible] readonly attribute unsigned long globalIndexOffset;
+
+  // The frameloader which owns this partial session history.
+  readonly attribute nsIFrameLoader ownerFrameLoader;
+
+  /**
+   * Notify that it's been added to a grouped session history. It also implies
+   * it's becoming the active partial history of the group.
+   *
+   * @param aOffset                The number of entries in preceding partial
+   *                               session histories.
+   */
+  void onAttachGroupedSessionHistory(in unsigned long aOffset);
+
+  /**
+   * Notify that one or more entries in its associated nsISHistory object
+   * have been changed (i.e. add / remove / replace). It's mainly used for
+   * cross-process case, since in the in-process case we can just register an
+   * nsISHistoryListener instead.
+   *
+   * @param aCount The number of entries in the associated session history.
+   *               It can be the same as the old value if entries were replaced.
+   */
+  void onSessionHistoryChange(in unsigned long aCount);
+
+  /**
+   * Notify that the partial session history has been swapped in as the active
+   * session history. Only an active session history can possibly add / remove /
+   * replace its history entries.
+   *
+   * @param aGlobalLength      The up-to-date global length.
+   * @param aTargetLocalIndex  The local index to navigate to.
+   */
+  void onActive(in unsigned long aGlobalLength, in unsigned long aTargetLocalIndex);
+
+  /**
+   * Notify that the partial session history has been swapped out and is no
+   * longer active.
+   */
+  void onDeactive();
+};
new file mode 100644
--- /dev/null
+++ b/docshell/shistory/nsIPartialSHistoryListener.idl
@@ -0,0 +1,24 @@
+/* -*- 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"
+
+/**
+ * Listener to handle cross partial nsISHistory navigation requests.
+ */
+[scriptable, uuid(be0cd2b6-6f03-4366-9fe2-184c914ff3df)]
+interface nsIPartialSHistoryListener : nsISupports
+{
+  /**
+   * Called when the navigation target belongs to another nsISHistory within
+   * the same nsIGroupedSHistory, and it needs to initiate cross nsISHistory
+   * navigation.
+   *
+   * @param aIndex The index of complete history to navigate to.
+   */
+   void onRequestCrossBrowserNavigation(in unsigned long aIndex);
+};
--- a/docshell/shistory/nsISHistory.idl
+++ b/docshell/shistory/nsISHistory.idl
@@ -3,16 +3,18 @@
  * 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;
 interface nsISHistoryListener;
 interface nsISimpleEnumerator;
+interface nsIPartialSHistoryListener;
+
 /**
  * An interface to the primary properties of the Session History
  * component. In an embedded browser environment, the nsIWebBrowser
  * object creates an instance of session history for each open window.
  * A handle to the session history object can be obtained from
  * nsIWebNavigation. In a non-embedded situation, the  owner of the
  * session history component must create a instance of it and set
  * it in the nsIWebNavigation object.
@@ -26,24 +28,51 @@ interface nsISimpleEnumerator;
 
 #define NS_SHISTORY_CONTRACTID "@mozilla.org/browser/shistory;1"
 %}
 
 [scriptable, uuid(7b807041-e60a-4384-935f-af3061d8b815)]
 interface nsISHistory: nsISupports
 {
   /**
+   * An attribute denoting whether the nsISHistory is associated to a grouped
+   * session history.
+   *
+   * The abstraction of grouped session history is implemented at
+   * nsIWebNavigation level, so those canGoBack / canGoForward / gotoIndex
+   * functions work transparently;
+   *
+   * On the other hand, nsISHistory works on partial session history directly.
+   * Unless otherwise specified, count / index attributes and parameters all
+   * indicate local count / index, so we won't mess up docshell.
+   */
+   readonly attribute bool isPartial;
+
+  /**
    * A readonly property of the interface that returns 
    * the number of toplevel documents currently available
    * in session history.
    */
    readonly attribute long count;
 
   /**
-   * A readonly property of the interface that returns 
+   * If isPartial, globalCount denotes the total number of entries in the
+   * grouped session history; Otherwise it has the same value as count.
+   */
+   readonly attribute long globalCount;
+
+  /**
+   * A readonly property which represents the difference between global indices
+   * of grouped session history and local indices of this particular session
+   * history object.
+   */
+   readonly attribute long globalIndexOffset;
+
+  /**
+   * A readonly property of the interface that returns
    * the index of the current document in session history.
    */
    readonly attribute long index;
 
   /**
    * A readonly property of the interface that returns 
    * the index of the last document that started to load and
    * didn't finished yet. When document finishes the loading
@@ -119,17 +148,23 @@ interface nsISHistory: nsISupports
    * @note                    A listener object must implement 
    *                          nsISHistoryListener and nsSupportsWeakReference
    * @see nsISHistoryListener
    * @see nsSupportsWeakReference
    */ 
    void removeSHistoryListener(in nsISHistoryListener aListener);
 
   /**
-   * Called to obtain a enumerator for all the  documents stored in 
+   * Set the listener to handle cross nsISHistory navigation when it works
+   * in "partial" mode.
+   */
+   void setPartialSHistoryListener(in nsIPartialSHistoryListener aListener);
+
+  /**
+   * Called to obtain a enumerator for all the  documents stored in
    * session history. The enumerator object thus returned by this method
    * can be traversed using nsISimpleEnumerator. 
    *
    * @note  To access individual history entries of the enumerator, perform the
    *        following steps:
    *        1) Call nsISHistory->GetSHistoryEnumerator() to obtain handle 
    *           the nsISimpleEnumerator object.
    *        2) Use nsISimpleEnumerator->GetNext() on the object returned
@@ -154,9 +189,33 @@ interface nsISHistory: nsISupports
    * @param aEntry            The entry to obtain the index of.
    *
    * @return                  <code>NS_OK</code> index for the history entry
    *                          is obtained successfully.
    *                          <code>NS_ERROR_FAILURE</code> Error in obtaining
    *                          index for the given history entry.
    */
    long getIndexOfEntry(in nsISHEntry aEntry);
+
+  /**
+   * Called when this nsISHistory has became the active history of a grouped
+   * session history.
+   *
+   * @param globalLength      The up to date number of entries in the grouped
+   *                          session history.
+   * @param targetIndex       The local index to navigate to.
+   */
+   void onPartialSessionHistoryActive(in long globalLength, in long targetIndex);
+
+  /**
+   * Called when this nsISHistory has became inactive history of a grouped
+   * session history.
+   */
+   void onPartialSessionHistoryDeactive();
+
+  /**
+   * Called when it's attached to a nsIGroupedSHistory instance.
+   *
+   * @param offset            The number of entries in the grouped session
+   *                          history before this session history object.
+   */
+   void onAttachGroupedSessionHistory(in long offset);
 };
--- a/docshell/shistory/nsISHistoryListener.idl
+++ b/docshell/shistory/nsISHistoryListener.idl
@@ -93,9 +93,16 @@ interface nsISHistoryListener : nsISuppo
    * Called when an entry is replaced in the session history. Entries are
    * replaced when navigating away from non-persistent history entries (such as
    * about pages) and when history.replaceState is called.
    *
    * @param aIndex        The index in session history of the entry being
   *                       replaced
    */
    void OnHistoryReplaceEntry(in long aIndex);
+
+   /**
+    * Called when nsISHistory::count has been updated. Unlike OnHistoryNewEntry
+    * and OnHistoryPurge which happen before the modifications are actually done
+    * and maybe cancellable, this function is called after these modifications.
+    */
+   void OnLengthChange(in long aCount);
 };
--- a/docshell/shistory/nsSHistory.cpp
+++ b/docshell/shistory/nsSHistory.cpp
@@ -221,16 +221,19 @@ EvictContentViewerForTransaction(nsISHTr
 }
 
 } // namespace
 
 nsSHistory::nsSHistory()
   : mIndex(-1)
   , mLength(0)
   , mRequestedIndex(-1)
+  , mIsPartial(false)
+  , mGlobalIndexOffset(0)
+  , mEntriesInFollowingPartialHistories(0)
   , mRootDocShell(nullptr)
 {
   // Add this new SHistory object to the list
   PR_APPEND_LINK(this, &gSHistoryList);
 }
 
 nsSHistory::~nsSHistory()
 {
@@ -414,40 +417,105 @@ nsSHistory::AddEntry(nsISHEntry* aSHEntr
   // parent will properly set the parent child relationship
   txn->SetPersist(aPersist);
   NS_ENSURE_SUCCESS(txn->Create(aSHEntry, currentTxn), NS_ERROR_FAILURE);
 
   // 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(OnLengthChange, (mLength));
+
+  // Much like how mLength works above, when changing our entries, all following
+  // partial histories should be purged, so we just reset the number to zero.
+  mEntriesInFollowingPartialHistories = 0;
 
   // 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);
   }
 
   RemoveDynEntries(mIndex - 1, mIndex);
   return NS_OK;
 }
 
+NS_IMETHODIMP
+nsSHistory::GetIsPartial(bool* aResult)
+{
+  NS_ENSURE_ARG_POINTER(aResult);
+  *aResult = mIsPartial;
+  return NS_OK;
+}
+
 /* Get size of the history list */
 NS_IMETHODIMP
 nsSHistory::GetCount(int32_t* aResult)
 {
   NS_ENSURE_ARG_POINTER(aResult);
   *aResult = mLength;
   return NS_OK;
 }
 
+NS_IMETHODIMP
+nsSHistory::GetGlobalCount(int32_t* aResult)
+{
+  NS_ENSURE_ARG_POINTER(aResult);
+  *aResult = mGlobalIndexOffset + mLength + mEntriesInFollowingPartialHistories;
+  return NS_OK;
+}
+
+NS_IMETHODIMP
+nsSHistory::GetGlobalIndexOffset(int32_t* aResult)
+{
+  NS_ENSURE_ARG_POINTER(aResult);
+  *aResult = mGlobalIndexOffset;
+  return NS_OK;
+}
+
+NS_IMETHODIMP
+nsSHistory::OnPartialSessionHistoryActive(int32_t aGlobalLength, int32_t aTargetIndex)
+{
+  NS_ENSURE_TRUE(mIsPartial, NS_ERROR_UNEXPECTED);
+
+  int32_t extraLength = aGlobalLength - mLength - mGlobalIndexOffset;
+  NS_ENSURE_TRUE(extraLength >= 0, NS_ERROR_UNEXPECTED);
+
+  if (extraLength != mEntriesInFollowingPartialHistories) {
+    mEntriesInFollowingPartialHistories = extraLength;
+  }
+
+  if (mIndex == aTargetIndex) {
+    // TODO When we finish OnPartialSessionHistoryDeactive, we'll need to active
+    // the suspended document here.
+
+    // Fire location change to update canGoBack / canGoForward.
+    NS_DispatchToCurrentThread(NewRunnableMethod(static_cast<nsDocShell*>(mRootDocShell),
+                                                 &nsDocShell::FireDummyOnLocationChange));
+    return NS_OK;
+  }
+
+  return LoadEntry(aTargetIndex, nsIDocShellLoadInfo::loadHistory,
+                   HIST_CMD_GOTOINDEX);
+}
+
+NS_IMETHODIMP
+nsSHistory::OnPartialSessionHistoryDeactive()
+{
+  NS_ENSURE_TRUE(mIsPartial, NS_ERROR_UNEXPECTED);
+
+  // TODO We need to suspend current document first. Much like what happens when
+  // loading a new page. Move the ownership of the document to nsISHEntry or so.
+  return NS_OK;
+}
+
 /* Get index of the history list */
 NS_IMETHODIMP
 nsSHistory::GetIndex(int32_t* aResult)
 {
   NS_PRECONDITION(aResult, "null out param?");
   *aResult = mIndex;
   return NS_OK;
 }
@@ -696,16 +764,20 @@ nsSHistory::PurgeHistory(int32_t aEntrie
     mListRoot = nextTxn;
     if (mListRoot) {
       mListRoot->SetPrev(nullptr);
     }
     cnt++;
   }
   mLength -= cnt;
   mIndex -= cnt;
+  NOTIFY_LISTENERS(OnLengthChange, (mLength));
+
+  // All following partial histories will be deleted in this case.
+  mEntriesInFollowingPartialHistories = 0;
 
   // 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;
   }
 
   if (mRootDocShell) {
@@ -737,16 +809,23 @@ nsSHistory::RemoveSHistoryListener(nsISH
 {
   // Make sure the listener that wants to be removed is the
   // one we have in store.
   nsWeakPtr listener = do_GetWeakReference(aListener);
   mListeners.RemoveElement(listener);
   return NS_OK;
 }
 
+NS_IMETHODIMP
+nsSHistory::SetPartialSHistoryListener(nsIPartialSHistoryListener* aListener)
+{
+  mPartialHistoryListener = do_GetWeakReference(aListener);
+  return NS_OK;
+}
+
 /* Replace an entry in the History list at a particular index.
  * Do not update index or count.
  */
 NS_IMETHODIMP
 nsSHistory::ReplaceEntry(int32_t aIndex, nsISHEntry* aReplaceEntry)
 {
   NS_ENSURE_ARG(aReplaceEntry);
   nsresult rv;
@@ -803,43 +882,53 @@ nsSHistory::EvictAllContentViewers()
 
   return NS_OK;
 }
 
 NS_IMETHODIMP
 nsSHistory::GetCanGoBack(bool* aCanGoBack)
 {
   NS_ENSURE_ARG_POINTER(aCanGoBack);
-  *aCanGoBack = false;
+
+  if (mGlobalIndexOffset) {
+    *aCanGoBack = true;
+    return NS_OK;
+  }
 
   int32_t index = -1;
   NS_ENSURE_SUCCESS(GetIndex(&index), NS_ERROR_FAILURE);
   if (index > 0) {
     *aCanGoBack = true;
+    return NS_OK;
   }
 
+  *aCanGoBack = false;
   return NS_OK;
 }
 
 NS_IMETHODIMP
 nsSHistory::GetCanGoForward(bool* aCanGoForward)
 {
   NS_ENSURE_ARG_POINTER(aCanGoForward);
-  *aCanGoForward = false;
+
+  if (mEntriesInFollowingPartialHistories) {
+    *aCanGoForward = true;
+    return NS_OK;
+  }
 
   int32_t index = -1;
   int32_t count = -1;
-
   NS_ENSURE_SUCCESS(GetIndex(&index), NS_ERROR_FAILURE);
   NS_ENSURE_SUCCESS(GetCount(&count), NS_ERROR_FAILURE);
-
   if (index >= 0 && index < (count - 1)) {
     *aCanGoForward = true;
+    return NS_OK;
   }
 
+  *aCanGoForward = false;
   return NS_OK;
 }
 
 NS_IMETHODIMP
 nsSHistory::GoBack()
 {
   bool canGoBack = false;
 
@@ -1353,16 +1442,18 @@ 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;
+    mEntriesInFollowingPartialHistories = 0;
+    NOTIFY_LISTENERS(OnLengthChange, (mLength));
     return true;
   }
   return false;
 }
 
 NS_IMETHODIMP_(void)
 nsSHistory::RemoveEntries(nsTArray<uint64_t>& aIDs, int32_t aStartIndex)
 {
@@ -1508,19 +1599,21 @@ nsSHistory::LoadURI(const char16_t* aURI
                     nsIURI* aReferringURI,
                     nsIInputStream* aPostStream,
                     nsIInputStream* aExtraHeaderStream)
 {
   return NS_OK;
 }
 
 NS_IMETHODIMP
-nsSHistory::GotoIndex(int32_t aIndex)
+nsSHistory::GotoIndex(int32_t aGlobalIndex)
 {
-  return LoadEntry(aIndex, nsIDocShellLoadInfo::loadHistory,
+  // We provide abstraction of grouped session history for nsIWebNavigation
+  // functions, so the index passed in here is global index.
+  return LoadEntry(aGlobalIndex - mGlobalIndexOffset, nsIDocShellLoadInfo::loadHistory,
                    HIST_CMD_GOTOINDEX);
 }
 
 nsresult
 nsSHistory::LoadNextPossibleEntry(int32_t aNewIndex, long aLoadType,
                                   uint32_t aHistCmd)
 {
   mRequestedIndex = -1;
@@ -1535,16 +1628,36 @@ nsSHistory::LoadNextPossibleEntry(int32_
 
 NS_IMETHODIMP
 nsSHistory::LoadEntry(int32_t aIndex, long aLoadType, uint32_t aHistCmd)
 {
   if (!mRootDocShell) {
     return NS_ERROR_FAILURE;
   }
 
+  if (aIndex < 0 || aIndex >= mLength) {
+    if (aIndex + mGlobalIndexOffset < 0) {
+      // The global index is negative.
+      return NS_ERROR_FAILURE;
+    }
+
+    if (aIndex - mLength >= mEntriesInFollowingPartialHistories) {
+      // The global index exceeds max possible value.
+      return NS_ERROR_FAILURE;
+    }
+
+    // The global index is valid. trigger cross browser navigation.
+    nsCOMPtr<nsIPartialSHistoryListener> listener =
+      do_QueryReferent(mPartialHistoryListener);
+    if (!listener) {
+      return NS_ERROR_FAILURE;
+    }
+    return listener->OnRequestCrossBrowserNavigation(aIndex + mGlobalIndexOffset);
+  }
+
   // Keep note of requested history index in mRequestedIndex.
   mRequestedIndex = aIndex;
 
   nsCOMPtr<nsISHEntry> prevEntry;
   GetEntryAtIndex(mIndex, false, getter_AddRefs(prevEntry));
 
   nsCOMPtr<nsISHEntry> nextEntry;
   GetEntryAtIndex(mRequestedIndex, false, getter_AddRefs(nextEntry));
@@ -1750,16 +1863,36 @@ NS_IMETHODIMP
 nsSHistory::GetSHistoryEnumerator(nsISimpleEnumerator** aEnumerator)
 {
   NS_ENSURE_ARG_POINTER(aEnumerator);
   RefPtr<nsSHEnumerator> iterator = new nsSHEnumerator(this);
   iterator.forget(aEnumerator);
   return NS_OK;
 }
 
+NS_IMETHODIMP
+nsSHistory::OnAttachGroupedSessionHistory(int32_t aOffset)
+{
+  NS_ENSURE_TRUE(!mIsPartial, NS_ERROR_UNEXPECTED);
+  NS_ENSURE_TRUE(aOffset >= 0, NS_ERROR_ILLEGAL_VALUE);
+
+  mIsPartial = true;
+  mGlobalIndexOffset = aOffset;
+
+  // The last attached history is always at the end of the group.
+  mEntriesInFollowingPartialHistories = 0;
+
+  // Setting grouped history info may change canGoBack / canGoForward.
+  // Send a location change to update these values.
+  NS_DispatchToCurrentThread(NewRunnableMethod(static_cast<nsDocShell*>(mRootDocShell),
+                                               &nsDocShell::FireDummyOnLocationChange));
+  return NS_OK;
+
+}
+
 nsSHEnumerator::nsSHEnumerator(nsSHistory* aSHistory) : mIndex(-1)
 {
   mSHistory = aSHistory;
 }
 
 nsSHEnumerator::~nsSHEnumerator()
 {
   mSHistory = nullptr;
--- a/docshell/shistory/nsSHistory.h
+++ b/docshell/shistory/nsSHistory.h
@@ -9,16 +9,17 @@
 
 #include "nsCOMPtr.h"
 #include "nsISHistory.h"
 #include "nsISHistoryInternal.h"
 #include "nsIWebNavigation.h"
 #include "nsISimpleEnumerator.h"
 #include "nsTObserverArray.h"
 #include "nsWeakPtr.h"
+#include "nsIPartialSHistoryListener.h"
 
 #include "prclist.h"
 
 class nsIDocShell;
 class nsSHEnumerator;
 class nsSHistoryObserver;
 class nsISHEntry;
 class nsISHTransaction;
@@ -41,17 +42,17 @@ public:
   static void UpdatePrefs();
 
   // Max number of total cached content viewers.  If the pref
   // browser.sessionhistory.max_total_viewers is negative, then
   // this value is calculated based on the total amount of memory.
   // Otherwise, it comes straight from the pref.
   static uint32_t GetMaxTotalViewers() { return sHistoryMaxTotalViewers; }
 
-protected:
+private:
   virtual ~nsSHistory();
   friend class nsSHEnumerator;
   friend class nsSHistoryObserver;
 
   // Could become part of nsIWebNavigation
   NS_IMETHOD GetTransactionAtIndex(int32_t aIndex, nsISHTransaction** aResult);
   nsresult LoadDifferingEntries(nsISHEntry* aPrevEntry, nsISHEntry* aNextEntry,
                                 nsIDocShell* aRootDocShell, long aLoadType,
@@ -75,28 +76,41 @@ protected:
   // content viewers to cache, based on amount of total memory
   static uint32_t CalcMaxTotalViewers();
 
   void RemoveDynEntries(int32_t aOldIndex, int32_t aNewIndex);
 
   nsresult LoadNextPossibleEntry(int32_t aNewIndex, long aLoadType,
                                  uint32_t aHistCmd);
 
-protected:
   // 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);
 
   nsCOMPtr<nsISHTransaction> mListRoot;
   int32_t mIndex;
   int32_t mLength;
   int32_t mRequestedIndex;
+
+  // Set to true if attached to a grouped session history.
+  bool mIsPartial;
+
+  // The number of entries before this session history object.
+  int32_t mGlobalIndexOffset;
+
+  // The number of entries after this session history object.
+  int32_t mEntriesInFollowingPartialHistories;
+
   // Session History listeners
   nsAutoTObserverArray<nsWeakPtr, 2> mListeners;
+
+  // Partial session history listener
+  nsWeakPtr mPartialHistoryListener;
+
   // Weak reference. Do not refcount this.
   nsIDocShell* mRootDocShell;
 
   // Max viewers allowed total, across all SHistory objects
   static int32_t sHistoryMaxTotalViewers;
 };
 
 class nsSHEnumerator : public nsISimpleEnumerator