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 362563 09b9bd35cf63d6b26fcbbcacee6043c8e82e38d8
parent 362562 1ba26e18f7cabe9f232780081d8b66366c7ddef4
child 362564 97ff05c0dc7d5ec0efb4078cc6e288e6f8ba3e90
push id6795
push userjlund@mozilla.com
push dateMon, 23 Jan 2017 14:19:46 +0000
treeherdermozilla-beta@76101b503191 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerssmaug
bugs1276553
milestone52.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
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