Bug 646641 - Part 1: SHEntries for the same document should share bfcache state. r=smaug
authorJustin Lebar <justin.lebar@gmail.com>
Mon, 09 May 2011 17:59:49 -0400
changeset 74469 883e581e0849aee53a3e7ed0b6a1a371e72ec856
parent 74468 49f809c8038457c583657f27955552b4e3ad0ea3
child 74470 cfea4859f45809693f6d00080ba427c5750c191d
push idunknown
push userunknown
push dateunknown
reviewerssmaug
bugs646641
milestone8.0a1
Bug 646641 - Part 1: SHEntries for the same document should share bfcache state. r=smaug
browser/components/sessionstore/src/nsSessionStore.js
browser/components/sessionstore/test/browser/browser_500328.js
content/base/public/nsIDocument.h
docshell/base/nsDocShell.cpp
docshell/build/nsDocShellModule.cpp
docshell/shistory/public/Makefile.in
docshell/shistory/public/nsIBFCacheEntry.idl
docshell/shistory/public/nsISHEntry.idl
docshell/shistory/public/nsISHistoryInternal.idl
docshell/shistory/src/Makefile.in
docshell/shistory/src/nsSHEntry.cpp
docshell/shistory/src/nsSHEntry.h
docshell/shistory/src/nsSHEntryShared.cpp
docshell/shistory/src/nsSHEntryShared.h
docshell/shistory/src/nsSHistory.cpp
docshell/test/Makefile.in
docshell/test/test_bfcache_plus_hash.html
dom/indexedDB/IndexedDatabaseManager.cpp
--- a/browser/components/sessionstore/src/nsSessionStore.js
+++ b/browser/components/sessionstore/src/nsSessionStore.js
@@ -1668,19 +1668,23 @@ SessionStoreService.prototype = {
         browser.__SS_data.entries[history.index] &&
         browser.__SS_data.entries[history.index].url == browser.currentURI.spec &&
         history.index < this._sessionhistory_max_entries - 1 && !aFullData) {
       tabData = browser.__SS_data;
       tabData.index = history.index + 1;
     }
     else if (history && history.count > 0) {
       try {
+        // bfCacheEntryMap maps a bfCacheEntry to a unique number.  It also
+        // contains a field 'max' which contains the maximum number in the map.
+        let bfCacheEntryMap = {'max': 0};
         for (var j = 0; j < history.count; j++) {
           let entry = this._serializeHistoryEntry(history.getEntryAtIndex(j, false),
-                                                  aFullData, aTab.pinned);
+                                                  aFullData, aTab.pinned,
+                                                  bfCacheEntryMap);
           tabData.entries.push(entry);
         }
         // If we make it through the for loop, then we're ok and we should clear
         // any indicator of brokenness.
         delete aTab.__SS_broken_history;
       }
       catch (ex) {
         // In some cases, getEntryAtIndex will throw. This seems to be due to
@@ -1759,20 +1763,24 @@ SessionStoreService.prototype = {
    * Get an object that is a serialized representation of a History entry
    * Used for data storage
    * @param aEntry
    *        nsISHEntry instance
    * @param aFullData
    *        always return privacy sensitive data (use with care)
    * @param aIsPinned
    *        the tab is pinned and should be treated differently for privacy
+   * @param aBFCacheEntryMap
+   *        a dictionary mapping BFCacheEntries to integers.  This dictionary
+   *        must also map 'max' to the largest number in the dictionary.
    * @returns object
    */
   _serializeHistoryEntry:
-    function sss_serializeHistoryEntry(aEntry, aFullData, aIsPinned) {
+    function sss_serializeHistoryEntry(aEntry, aFullData,
+                                       aIsPinned, aBFCacheEntryMap) {
     var entry = { url: aEntry.URI.spec };
 
     try {
       entry._host = aEntry.URI.host;
       entry._scheme = aEntry.URI.scheme;
     }
     catch (ex) {
       // We just won't attempt to get cookies for this entry.
@@ -1853,18 +1861,24 @@ SessionStoreService.prototype = {
         // We can stop doing base64 encoding once our serialization into JSON
         // is guaranteed to handle all chars in strings, including embedded
         // nulls.
         entry.owner_b64 = btoa(String.fromCharCode.apply(null, ownerBytes));
       }
       catch (ex) { debug(ex); }
     }
 
-    if (aEntry.docIdentifier) {
-      entry.docIdentifier = aEntry.docIdentifier;
+    if (aBFCacheEntryMap[aEntry.BFCacheEntry]) {
+      entry.docIdentifier = aBFCacheEntryMap[aEntry.BFCacheEntry];
+    }
+    else {
+      let id = aBFCacheEntryMap['max'] + 1;
+      entry.docIdentifier = id;
+      aBFCacheEntryMap['max'] = id;
+      aBFCacheEntryMap[aEntry.BFCacheEntry] = id;
     }
 
     if (aEntry.stateData != null) {
       entry.structuredCloneState = aEntry.stateData.getDataAsBase64();
       entry.structuredCloneVersion = aEntry.stateData.formatVersion;
     }
 
     if (!(aEntry instanceof Ci.nsISHContainer)) {
@@ -1872,17 +1886,17 @@ SessionStoreService.prototype = {
     }
     
     if (aEntry.childCount > 0) {
       entry.children = [];
       for (var i = 0; i < aEntry.childCount; i++) {
         var child = aEntry.GetChildAt(i);
         if (child) {
           entry.children.push(this._serializeHistoryEntry(child, aFullData,
-                                                          aIsPinned));
+                                                          aIsPinned, aBFCacheEntryMap));
         }
         else { // to maintain the correct frame order, insert a dummy entry 
           entry.children.push({ url: "about:blank" });
         }
         // don't try to restore framesets containing wyciwyg URLs (cf. bug 424689 and bug 450595)
         if (/^wyciwyg:\/\//.test(entry.children[i].url)) {
           delete entry.children;
           break;
@@ -2955,17 +2969,16 @@ SessionStoreService.prototype = {
                        getEntryAtIndex(activeIndex, false).
                        QueryInterface(Ci.nsISHEntry);
 
       // restore those aspects of the currently active documents which are not
       // preserved in the plain history entries (mainly scroll state and text data)
       browser.__SS_restore_data = tabData.entries[activeIndex] || {};
       browser.__SS_restore_pageStyle = tabData.pageStyle || "";
       browser.__SS_restore_tab = aTab;
-      browser.__SS_restore_docIdentifier = curSHEntry.docIdentifier;
 
       didStartLoad = true;
       try {
         // In order to work around certain issues in session history, we need to
         // force session history to update its internal index and call reload
         // instead of gotoIndex. See bug 597315.
         browser.webNavigation.sessionHistory.getEntryAtIndex(activeIndex, true);
         browser.webNavigation.sessionHistory.reloadCurrentEntry();
@@ -3108,34 +3121,26 @@ SessionStoreService.prototype = {
       var postdata = atob(aEntry.postdata_b64);
       var stream = Cc["@mozilla.org/io/string-input-stream;1"].
                    createInstance(Ci.nsIStringInputStream);
       stream.setData(postdata, postdata.length);
       shEntry.postData = stream;
     }
 
     if (aEntry.docIdentifier) {
-      // Get a new document identifier for this entry to ensure that history
-      // entries after a session restore are considered to have different
-      // documents from the history entries before the session restore.
-      // Document identifiers are 64-bit ints, so JS will loose precision and
-      // start assigning all entries the same doc identifier if these ever get
-      // large enough.
-      //
-      // It's a potential security issue if document identifiers aren't
-      // globally unique, but shEntry.setUniqueDocIdentifier() below guarantees
-      // that we won't re-use a doc identifier within a given instance of the
-      // application.
-      let ident = aDocIdentMap[aEntry.docIdentifier];
-      if (!ident) {
-        shEntry.setUniqueDocIdentifier();
-        aDocIdentMap[aEntry.docIdentifier] = shEntry.docIdentifier;
+      // If we have a serialized document identifier, try to find an SHEntry
+      // which matches that doc identifier and adopt that SHEntry's
+      // BFCacheEntry.  If we don't find a match, insert shEntry as the match
+      // for the document identifier.
+      let matchingEntry = aDocIdentMap[aEntry.docIdentifier];
+      if (!matchingEntry) {
+        aDocIdentMap[aEntry.docIdentifier] = shEntry;
       }
       else {
-        shEntry.docIdentifier = ident;
+        shEntry.adoptBFCacheEntry(matchingEntry);
       }
     }
 
     if (aEntry.owner_b64) {
       var ownerInput = Cc["@mozilla.org/io/string-input-stream;1"].
                        createInstance(Ci.nsIStringInputStream);
       var binaryData = atob(aEntry.owner_b64);
       ownerInput.setData(binaryData, binaryData.length);
@@ -3261,29 +3266,22 @@ SessionStoreService.prototype = {
     // don't restore text data and scrolling state if the user has navigated
     // away before the loading completed (except for in-page navigation)
     if (hasExpectedURL(aEvent.originalTarget, aBrowser.__SS_restore_data.url)) {
       var content = aEvent.originalTarget.defaultView;
       restoreTextDataAndScrolling(content, aBrowser.__SS_restore_data, "");
       aBrowser.markupDocumentViewer.authorStyleDisabled = selectedPageStyle == "_nostyle";
     }
 
-    if (aBrowser.__SS_restore_docIdentifier) {
-      let sh = aBrowser.webNavigation.sessionHistory;
-      sh.getEntryAtIndex(sh.index, false).QueryInterface(Ci.nsISHEntry).
-         docIdentifier = aBrowser.__SS_restore_docIdentifier;
-    }
-
     // notify the tabbrowser that this document has been completely restored
     this._sendTabRestoredNotification(aBrowser.__SS_restore_tab);
 
     delete aBrowser.__SS_restore_data;
     delete aBrowser.__SS_restore_pageStyle;
     delete aBrowser.__SS_restore_tab;
-    delete aBrowser.__SS_restore_docIdentifier;
   },
 
   /**
    * Restore visibility and dimension features to a window
    * @param aWindow
    *        Window reference
    * @param aWinData
    *        Object containing session data for the window
--- a/browser/components/sessionstore/test/browser/browser_500328.js
+++ b/browser/components/sessionstore/test/browser/browser_500328.js
@@ -111,23 +111,23 @@ function test() {
 
     tabBrowser.loadURI("http://example.com", null, null);
 
     tabBrowser.addEventListener("load", function(aEvent) {
       tabBrowser.removeEventListener("load", arguments.callee, true);
 
       // After these push/replaceState calls, the window should have three
       // history entries:
-      //   testURL (state object: null)      <-- oldest
-      //   testURL (state object: {obj1:1})
-      //   page2   (state object: {obj3:/^a$/})  <-- newest
+      //   testURL        (state object: null)          <-- oldest
+      //   testURL        (state object: {obj1:1})
+      //   testURL?page2  (state object: {obj3:/^a$/})  <-- newest
       let contentWindow = tab.linkedBrowser.contentWindow;
       let history = contentWindow.history;
       history.pushState({obj1:1}, "title-obj1");
-      history.pushState({obj2:2}, "title-obj2", "page2");
+      history.pushState({obj2:2}, "title-obj2", "?page2");
       history.replaceState({obj3:/^a$/}, "title-obj3");
 
       let state = ss.getTabState(tab);
       gBrowser.removeTab(tab);
 
       // Restore the state into a new tab.  Things don't work well when we
       // restore into the old tab, but that's not a real use case anyway.
       let tab2 = gBrowser.addTab("about:blank");
--- a/content/base/public/nsIDocument.h
+++ b/content/base/public/nsIDocument.h
@@ -64,16 +64,17 @@
 #ifdef MOZ_SMIL
 #include "nsSMILAnimationController.h"
 #endif // MOZ_SMIL
 #include "nsIScriptGlobalObject.h"
 #include "nsIDocumentEncoder.h"
 #include "nsIAnimationFrameListener.h"
 #include "nsEventStates.h"
 #include "nsIStructuredCloneContainer.h"
+#include "nsIBFCacheEntry.h"
 #include "nsDOMMemoryReporter.h"
 
 class nsIContent;
 class nsPresContext;
 class nsIPresShell;
 class nsIDocShell;
 class nsStyleSet;
 class nsIStyleSheet;
@@ -120,19 +121,19 @@ class Loader;
 
 namespace dom {
 class Link;
 class Element;
 } // namespace dom
 } // namespace mozilla
 
 
-#define NS_IDOCUMENT_IID \
-{ 0xfac563fb, 0x2b6a, 0x4ac8, \
- { 0x85, 0xf7, 0xd5, 0x14, 0x4b, 0x3e, 0xce, 0x78 } }
+#define NS_IDOCUMENT_IID      \
+{ 0x455e4d79, 0x756b, 0x4f73,  \
+ { 0x95, 0xea, 0x3f, 0xf6, 0x0c, 0x6a, 0x8c, 0xa6 } }
 
 // Flag for AddStyleSheet().
 #define NS_STYLESHEET_FROM_CATALOG                (1 << 0)
 
 // Document states
 
 // RTL locale: specific to the XUL localedir attribute
 #define NS_DOCUMENT_STATE_RTL_LOCALE              NS_DEFINE_EVENT_STATE_MACRO(0)
@@ -475,21 +476,25 @@ public:
                                nsIPresShell** aInstancePtrResult) = 0;
   virtual void DeleteShell() = 0;
 
   nsIPresShell* GetShell() const
   {
     return GetBFCacheEntry() ? nsnull : mPresShell;
   }
 
-  void SetBFCacheEntry(nsISHEntry* aSHEntry) {
-    mSHEntry = aSHEntry;
+  void SetBFCacheEntry(nsIBFCacheEntry* aEntry)
+  {
+    mBFCacheEntry = aEntry;
   }
 
-  nsISHEntry* GetBFCacheEntry() const { return mSHEntry; }
+  nsIBFCacheEntry* GetBFCacheEntry() const
+  {
+    return mBFCacheEntry;
+  }
 
   /**
    * Return the parent document of this document. Will return null
    * unless this document is within a compound document and has a
    * parent. Note that this parent chain may cross chrome boundaries.
    */
   nsIDocument *GetParentDocument() const
   {
@@ -1733,19 +1738,19 @@ protected:
   // Weak reference to mScriptGlobalObject QI:d to nsPIDOMWindow,
   // updated on every set of mSecriptGlobalObject.
   nsPIDOMWindow *mWindow;
 
   nsCOMPtr<nsIDocumentEncoder> mCachedEncoder;
 
   AnimationListenerList mAnimationFrameListeners;
 
-  // The session history entry in which we're currently bf-cached. Non-null
-  // if and only if we're currently in the bfcache.
-  nsISHEntry* mSHEntry;
+  // This object allows us to evict ourself from the back/forward cache.  The
+  // pointer is non-null iff we're currently in the bfcache.
+  nsIBFCacheEntry *mBFCacheEntry;
 
   // Our base target.
   nsString mBaseTarget;
 
   nsCOMPtr<nsIStructuredCloneContainer> mStateObjectContainer;
   nsCOMPtr<nsIVariant> mStateObjectCached;
 };
 
--- a/docshell/base/nsDocShell.cpp
+++ b/docshell/base/nsDocShell.cpp
@@ -4131,20 +4131,20 @@ nsDocShell::LoadErrorPage(nsIURI *aURI, 
                 spec.get(), NS_ConvertUTF16toUTF8(aURL).get(), chanName.get()));
     }
 #endif
     mFailedChannel = aFailedChannel;
     mFailedURI = aURI;
     mFailedLoadType = mLoadType;
 
     if (mLSHE) {
-        // If we don't give mLSHE a new doc identifier here, when we go back or
-        // forward to another SHEntry with the same doc identifier, the error
-        // page will persist.
-        mLSHE->SetUniqueDocIdentifier();
+        // Abandon mLSHE's BFCache entry and create a new one.  This way, if
+        // we go back or forward to another SHEntry with the same doc
+        // identifier, the error page won't persist.
+        mLSHE->AbandonBFCacheEntry();
     }
 
     nsCAutoString url;
     nsCAutoString charset;
     if (aURI)
     {
         nsresult rv = aURI->GetSpec(url);
         rv |= aURI->GetOriginCharset(charset);
@@ -4407,20 +4407,20 @@ nsDocShell::LoadPage(nsISupports *aPageD
     }
 
     // Now clone shEntryIn, since we might end up modifying it later on, and we
     // want a page descriptor to be reusable.
     nsCOMPtr<nsISHEntry> shEntry;
     nsresult rv = shEntryIn->Clone(getter_AddRefs(shEntry));
     NS_ENSURE_SUCCESS(rv, rv);
 
-    // Give our cloned shEntry a new document identifier so this load is
-    // independent of all other loads.  (This is important, in particular,
-    // for bugs 582795 and 585298.)
-    rv = shEntry->SetUniqueDocIdentifier();
+    // Give our cloned shEntry a new bfcache entry so this load is independent
+    // of all other loads.  (This is important, in particular, for bugs 582795
+    // and 585298.)
+    rv = shEntry->AbandonBFCacheEntry();
     NS_ENSURE_SUCCESS(rv, rv);
 
     //
     // load the page as view-source
     //
     if (nsIWebPageDescriptor::DISPLAY_AS_SOURCE == aDisplayType) {
         nsCOMPtr<nsIURI> oldUri, newUri;
         nsCString spec, newSpec;
@@ -8398,27 +8398,25 @@ nsDocShell::InternalLoad(nsIURI * aURI,
                                            curBeforeHash, curHash) :
             NS_ERROR_FAILURE;
         splitRv2 = nsContentUtils::SplitURIAtHash(aURI, newBeforeHash, newHash);
 
         PRBool sameExceptHashes = NS_SUCCEEDED(splitRv1) &&
                                   NS_SUCCEEDED(splitRv2) &&
                                   curBeforeHash.Equals(newBeforeHash);
 
-        PRBool sameDocIdent = PR_FALSE;
+        // XXX rename
+        PRBool sameDocument = PR_FALSE;
         if (mOSHE && aSHEntry) {
             // We're doing a history load.
 
-            PRUint64 ourDocIdent, otherDocIdent;
-            mOSHE->GetDocIdentifier(&ourDocIdent);
-            aSHEntry->GetDocIdentifier(&otherDocIdent);
-            sameDocIdent = (ourDocIdent == otherDocIdent);
+            mOSHE->SharesDocumentWith(aSHEntry, &sameDocument);
 
 #ifdef DEBUG
-            if (sameDocIdent) {
+            if (sameDocument) {
                 nsCOMPtr<nsIInputStream> currentPostData;
                 mOSHE->GetPostData(getter_AddRefs(currentPostData));
                 NS_ASSERTION(currentPostData == aPostData,
                              "Different POST data for entries for the same page?");
             }
 #endif
         }
 
@@ -8431,17 +8429,17 @@ nsDocShell::InternalLoad(nsIURI * aURI,
         //
         //  b) we're navigating to a new shentry whose URI differs from the
         //     current URI only in its hash, the new hash is non-empty, and
         //     we're not doing a POST.
         //
         // The restriction tha the SHEntries in (a) must be different ensures
         // that history.go(0) and the like trigger full refreshes, rather than
         // short-circuited loads.
-        PRBool doShortCircuitedLoad = (sameDocIdent && mOSHE != aSHEntry) ||
+        PRBool doShortCircuitedLoad = (sameDocument && mOSHE != aSHEntry) ||
                                       (!aSHEntry && aPostData == nsnull &&
                                        sameExceptHashes && !newHash.IsEmpty());
 
         // Fire a hashchange event if we're doing a short-circuited load and the
         // URIs differ only in their hashes.
         PRBool doHashchange = doShortCircuitedLoad &&
                               sameExceptHashes &&
                               !curHash.Equals(newHash);
@@ -8482,32 +8480,35 @@ nsDocShell::InternalLoad(nsIURI * aURI,
                 mOSHE->GetOwner(getter_AddRefs(owner));
             }
             // Pass true for aCloneSHChildren, since we're not
             // changing documents here, so all of our subframes are
             // still relevant to the new session history entry.
             OnNewURI(aURI, nsnull, owner, mLoadType, PR_TRUE, PR_TRUE, PR_TRUE);
 
             nsCOMPtr<nsIInputStream> postData;
-            PRUint64 docIdent = PRUint64(-1);
             nsCOMPtr<nsISupports> cacheKey;
 
             if (mOSHE) {
                 /* save current position of scroller(s) (bug 59774) */
                 mOSHE->SetScrollPosition(cx, cy);
                 // Get the postdata and page ident from the current page, if
                 // the new load is being done via normal means.  Note that
                 // "normal means" can be checked for just by checking for
                 // LOAD_CMD_NORMAL, given the loadType and allowScroll check
                 // above -- it filters out some LOAD_CMD_NORMAL cases that we
                 // wouldn't want here.
                 if (aLoadType & LOAD_CMD_NORMAL) {
                     mOSHE->GetPostData(getter_AddRefs(postData));
-                    mOSHE->GetDocIdentifier(&docIdent);
                     mOSHE->GetCacheKey(getter_AddRefs(cacheKey));
+
+                    // Link our new SHEntry to the old SHEntry's back/forward
+                    // cache data, since the two SHEntries correspond to the
+                    // same document.
+                    mLSHE->AdoptBFCacheEntry(mOSHE);
                 }
             }
 
             /* Assign mOSHE to mLSHE. This will either be a new entry created
              * by OnNewURI() for normal loads or aSHEntry for history loads.
              */
             if (mLSHE) {
                 SetHistoryEntry(&mOSHE, mLSHE);
@@ -8517,21 +8518,16 @@ nsDocShell::InternalLoad(nsIURI * aURI,
                 // page will restore the appropriate postData.
                 if (postData)
                     mOSHE->SetPostData(postData);
 
                 // Make sure we won't just repost without hitting the
                 // cache first
                 if (cacheKey)
                     mOSHE->SetCacheKey(cacheKey);
-
-                // Propagate our document identifier to the new mOSHE so that
-                // we'll know it's related by an anchor navigation or pushState.
-                if (docIdent != PRUint64(-1))
-                    mOSHE->SetDocIdentifier(docIdent);
             }
 
             /* restore previous position of scroller(s), if we're moving
              * back in history (bug 59774)
              */
             if (mOSHE && (aLoadType == LOAD_HISTORY || aLoadType == LOAD_RELOAD_NORMAL))
             {
                 nscoord bx, by;
@@ -8565,37 +8561,37 @@ nsDocShell::InternalLoad(nsIURI * aURI,
                 if (history) {
                     history->SetURITitle(aURI, mTitle);
                 }
                 else if (mGlobalHistory) {
                     mGlobalHistory->SetPageTitle(aURI, mTitle);
                 }
             }
 
-            if (sameDocIdent) {
+            if (sameDocument) {
                 // Set the doc's URI according to the new history entry's URI
                 nsCOMPtr<nsIURI> newURI;
                 mOSHE->GetURI(getter_AddRefs(newURI));
                 NS_ENSURE_TRUE(newURI, NS_ERROR_FAILURE);
                 nsCOMPtr<nsIDocument> doc =
                   do_GetInterface(GetAsSupports(this));
                 NS_ENSURE_TRUE(doc, NS_ERROR_FAILURE);
 
                 doc->SetDocumentURI(newURI);
             }
 
             SetDocCurrentStateObj(mOSHE);
 
             // Dispatch the popstate and hashchange events, as appropriate.
             nsCOMPtr<nsPIDOMWindow> window = do_QueryInterface(mScriptGlobal);
             if (window) {
-                // Need the doHashchange check here since sameDocIdent is
+                // Need the doHashchange check here since sameDocument is
                 // false if we're navigating to a new shentry (i.e. a aSHEntry
                 // is null), such as when clicking a <a href="#foo">.
-                if (sameDocIdent || doHashchange) {
+                if (sameDocument || doHashchange) {
                   window->DispatchSyncPopState();
                 }
 
                 if (doHashchange) {
                   // Make sure to use oldURI here, not mCurrentURI, because by
                   // now, mCurrentURI has changed!
                   window->DispatchAsyncHashchange(oldURI, aURI);
                 }
@@ -9415,21 +9411,21 @@ nsDocShell::OnNewURI(nsIURI * aURI, nsIC
 
         if (httpChannel) {
             nsCOMPtr<nsIUploadChannel> uploadChannel(do_QueryInterface(httpChannel));
             if (uploadChannel) {
                 uploadChannel->GetUploadStream(getter_AddRefs(inputStream));
             }
 
             // If the response status indicates an error, unlink this session
-            // history entry from any entries sharing its doc ident.
+            // history entry from any entries sharing its document.
             PRUint32 responseStatus;
             nsresult rv = httpChannel->GetResponseStatus(&responseStatus);
             if (mLSHE && NS_SUCCEEDED(rv) && responseStatus >= 400) {
-                mLSHE->SetUniqueDocIdentifier();
+                mLSHE->AbandonBFCacheEntry();
             }
         }
     }
     /* Create SH Entry (mLSHE) only if there is a  SessionHistory object (mSessionHistory) in
      * the current frame or in the root docshell
      */
     nsCOMPtr<nsISHistory> rootSH = mSessionHistory;
     if (!rootSH) {
@@ -9830,21 +9826,19 @@ nsDocShell::AddState(nsIVariant *aData, 
         // Since we're not changing which page we have loaded, pass
         // true for aCloneChildren.
         rv = AddToSessionHistory(newURI, nsnull, nsnull, PR_TRUE,
                                  getter_AddRefs(newSHEntry));
         NS_ENSURE_SUCCESS(rv, rv);
 
         NS_ENSURE_TRUE(newSHEntry, NS_ERROR_FAILURE);
 
-        // Set the new SHEntry's document identifier, if we can.
-        PRUint64 ourDocIdent;
-        NS_ENSURE_SUCCESS(oldOSHE->GetDocIdentifier(&ourDocIdent),
-                          NS_ERROR_FAILURE);
-        NS_ENSURE_SUCCESS(newSHEntry->SetDocIdentifier(ourDocIdent),
+        // Link the new SHEntry to the old SHEntry's BFCache entry, since the
+        // two entries correspond to the same document.
+        NS_ENSURE_SUCCESS(newSHEntry->AdoptBFCacheEntry(oldOSHE),
                           NS_ERROR_FAILURE);
 
         // Set the new SHEntry's title (bug 655273).
         nsString title;
         mOSHE->GetTitle(getter_Copies(title));
         newSHEntry->SetTitle(title);
 
         // AddToSessionHistory may not modify mOSHE.  In case it doesn't,
--- a/docshell/build/nsDocShellModule.cpp
+++ b/docshell/build/nsDocShellModule.cpp
@@ -61,16 +61,17 @@
 #include "nsExternalSharingAppService.h"
 #endif
 #if defined(ANDROID)
 #include "nsExternalURLHandlerService.h"
 #endif
 
 // session history
 #include "nsSHEntry.h"
+#include "nsSHEntryShared.h"
 #include "nsSHistory.h"
 #include "nsSHTransaction.h"
 
 // download history
 #include "nsDownloadHistory.h"
 
 static PRBool gInitialized = PR_FALSE;
 
@@ -82,25 +83,25 @@ Initialize()
   if (gInitialized) {
     return NS_OK;
   }
   gInitialized = PR_TRUE;
 
   nsresult rv = nsSHistory::Startup();
   NS_ENSURE_SUCCESS(rv, rv);
 
-  rv = nsSHEntry::Startup();
-  return rv;
+  nsSHEntryShared::Startup();
+  return NS_OK;
 }
 
 static void
 Shutdown()
 {
   nsSHistory::Shutdown();
-  nsSHEntry::Shutdown();
+  nsSHEntryShared::Shutdown();
   gInitialized = PR_FALSE;
 }
 
 // docshell
 NS_GENERIC_FACTORY_CONSTRUCTOR_INIT(nsDocShell, Init)
 NS_GENERIC_FACTORY_CONSTRUCTOR(nsDefaultURIFixup)
 NS_GENERIC_FACTORY_CONSTRUCTOR_INIT(nsWebNavigationInfo, Init)
 
--- a/docshell/shistory/public/Makefile.in
+++ b/docshell/shistory/public/Makefile.in
@@ -51,12 +51,13 @@ SDK_XPIDLSRCS   = \
                   nsISHistoryListener.idl   \
                   $(NULL)
 
 XPIDLSRCS	= \
 		  nsISHEntry.idl        \
                   nsISHContainer.idl    \
                   nsISHTransaction.idl  \
                   nsISHistoryInternal.idl \
+		  nsIBFCacheEntry.idl \
                   $(NULL)
 
 include $(topsrcdir)/config/rules.mk
 
new file mode 100644
--- /dev/null
+++ b/docshell/shistory/public/nsIBFCacheEntry.idl
@@ -0,0 +1,45 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ * http://www.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is mozilla.org code.
+ *
+ * The Initial Developer of the Original Code is the Mozilla Foundation.
+ * Portions created by the Initial Developer are Copyright (C) 2011
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either of the GNU General Public License Version 2 or later (the "GPL"),
+ * or the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+#include "nsISupports.idl"
+
+/**
+ * This interface lets you evict a document from the back/forward cache.
+ */
+[scriptable, uuid(27A326F6-5D66-463D-8CDC-49394A8CB7D6)]
+interface nsIBFCacheEntry : nsISupports
+{
+  void RemoveFromBFCacheSync();
+  void RemoveFromBFCacheAsync();
+};
--- a/docshell/shistory/public/nsISHEntry.idl
+++ b/docshell/shistory/public/nsISHEntry.idl
@@ -46,25 +46,28 @@
 
 interface nsILayoutHistoryState;
 interface nsIContentViewer;
 interface nsIURI;
 interface nsIInputStream;
 interface nsIDocShellTreeItem;
 interface nsISupportsArray;
 interface nsIStructuredCloneContainer;
+interface nsIBFCacheEntry;
+
 %{C++
 struct nsIntRect;
 class nsDocShellEditorData;
+class nsSHEntryShared;
 %}
 [ref] native nsIntRect(nsIntRect);
 [ptr] native nsDocShellEditorDataPtr(nsDocShellEditorData);
-
+[ptr] native nsSHEntryShared(nsSHEntryShared);
 
-[scriptable, uuid(b92d403e-f5ec-4b81-b0e3-6e6c241cef2d)]
+[scriptable, uuid(6443FD72-A50F-4B8B-BB82-BB1FA04CB15D)]
 interface nsISHEntry : nsIHistoryEntry
 {
     /** URI for the document */
     void setURI(in nsIURI aURI);
 
     /** Referrer URI */
     attribute nsIURI referrerURI;
 
@@ -135,31 +138,16 @@ interface nsISHEntry : nsIHistoryEntry
     attribute unsigned long loadType;
 
     /**
      * An ID to help identify this entry from others during
      * subframe navigation
      */
     attribute unsigned long ID;
 
-    /**
-     * docIdentifier is an integer that should be the same for two entries
-     * attached to the same docshell if and only if the two entries are entries
-     * for the same document.  In practice, two entries A and B will have the
-     * same docIdentifier if we arrived at B by clicking an anchor link in A or
-     * if B was created by A's calling history.pushState().
-     */
-    attribute unsigned long long docIdentifier;
-
-    /**
-     * Changes this entry's doc identifier to a new value which is unique
-     * among those of all other entries.
-     */
-    void setUniqueDocIdentifier();
-
     /** attribute to set and get the cache key for the entry */
     attribute nsISupports cacheKey;
 
     /** attribute to indicate whether layoutHistoryState should be saved */
     attribute boolean saveLayoutStateFlag;
 
     /** attribute to indicate whether the page is already expired in cache */
     attribute boolean expirationStatus;
@@ -247,28 +235,69 @@ interface nsISHEntry : nsIHistoryEntry
      * when isDynamicallyAdded is called on it.
      */
     boolean hasDynamicallyAddedChild();
 
     /**
      * The history ID of the docshell.
      */
     attribute unsigned long long docshellID;
+
+    readonly attribute nsIBFCacheEntry BFCacheEntry;
+
+    /**
+     * Does this SHEntry point to the given BFCache entry?  If so, evicting
+     * the BFCache entry will evict the SHEntry, since the two entries
+     * correspond to the same document.
+     */
+    [notxpcom, noscript]
+    boolean hasBFCacheEntry(in nsIBFCacheEntry aEntry);
+
+    /**
+     * Adopt aEntry's BFCacheEntry, so now both this and aEntry point to
+     * aEntry's BFCacheEntry.
+     */
+    void adoptBFCacheEntry(in nsISHEntry aEntry);
+
+    /**
+     * Create a new BFCache entry and drop our reference to our old one.  This
+     * call unlinks this SHEntry from any other SHEntries for its document.
+     */
+    void abandonBFCacheEntry();
+
+    /**
+     * Does this SHEntry correspond to the same document as aEntry?  This is
+     * true iff the two SHEntries have the same BFCacheEntry.  So in
+     * particular, sharesDocumentWith(aEntry) is guaranteed to return true if
+     * it's preceeded by a call to adoptBFCacheEntry(aEntry).
+     */
+    boolean sharesDocumentWith(in nsISHEntry aEntry);
 };
 
 [scriptable, uuid(bb66ac35-253b-471f-a317-3ece940f04c5)]
 interface nsISHEntryInternal : nsISupports
 {
     [notxpcom] void RemoveFromBFCacheAsync();
     [notxpcom] void RemoveFromBFCacheSync();
 
     /**
      * A number that is assigned by the sHistory when the entry is activated
      */
     attribute unsigned long lastTouched;
+
+    /**
+     * Some state, particularly that related to the back/forward cache, is
+     * shared between SHEntries which correspond to the same document.  This
+     * method gets a pointer to that shared state.
+     *
+     * This shared state is the SHEntry's BFCacheEntry.  So
+     * hasBFCacheEntry(getSharedState()) is guaranteed to return true.
+     */
+    [noscript, notxpcom]
+    nsSHEntryShared getSharedState();
 };
 
 %{ C++
 // {BFD1A791-AD9F-11d3-BDC7-0050040A9B44}
 #define NS_SHENTRY_CID \
 {0xbfd1a791, 0xad9f, 0x11d3, {0xbd, 0xc7, 0x0, 0x50, 0x4, 0xa, 0x9b, 0x44}}
 
 #define NS_SHENTRY_CONTRACTID \
--- a/docshell/shistory/public/nsISHistoryInternal.idl
+++ b/docshell/shistory/public/nsISHistoryInternal.idl
@@ -52,17 +52,17 @@ interface nsIDocShell;
 #define NS_SHISTORY_INTERNAL_CONTRACTID "@mozilla.org/browser/shistory-internal;1"
 
 template<class E, class A> class nsTArray;
 struct nsTArrayDefaultAllocator;
 %}
 
 [ref] native nsDocshellIDArray(nsTArray<PRUint64, nsTArrayDefaultAllocator>);
 
-[scriptable, uuid(2dede933-25e1-47a3-8f61-0127c785ea01)]
+[scriptable, uuid(AFE77866-8859-4492-B131-4C58167E1630)]
 interface nsISHistoryInternal: nsISupports
 {
   /**
    * Add a new Entry to the History List
    * @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.
@@ -101,20 +101,20 @@ interface nsISHistoryInternal: nsISuppor
    * is no more than gHistoryMaxViewers.  Also, count
    * total number of content viewers globally and evict one if we are over
    * our total max.  This is always called in Show(), after we destroy
    * the previous viewer.
    */
    void evictContentViewers(in long previousIndex, in long index);
    
    /**
-    * Evict the content viewer associated with a session history entry
+    * Evict the content viewer associated with a bfcache entry
     * that has timed out.
     */
-   void evictExpiredContentViewerForEntry(in nsISHEntry aEntry);
+   void evictExpiredContentViewerForEntry(in nsIBFCacheEntry aEntry);
 
    /**
     * Evict all the content viewers in this session history
     */
    void evictAllContentViewers();
 
    /**
     * Removes entries from the history if their docshellID is in
--- a/docshell/shistory/src/Makefile.in
+++ b/docshell/shistory/src/Makefile.in
@@ -43,17 +43,20 @@ VPATH		= @srcdir@
 
 include $(DEPTH)/config/autoconf.mk
 
 MODULE		= shistory
 LIBRARY_NAME	= shistory_s
 FORCE_STATIC_LIB = 1
 LIBXUL_LIBRARY	= 1
 
+EXPORTS		= nsSHEntryShared.h \
+		  $(NULL)
 
 CPPSRCS		= nsSHEntry.cpp        \
             nsSHTransaction.cpp   \
             nsSHistory.cpp \
+	    nsSHEntryShared.cpp \
             $(NULL)
 
 include $(topsrcdir)/config/rules.mk
 
 LOCAL_INCLUDES += -I$(srcdir)/../../base
--- a/docshell/shistory/src/nsSHEntry.cpp
+++ b/docshell/shistory/src/nsSHEntry.cpp
@@ -31,171 +31,87 @@
  * use your version of this file under the terms of the MPL, indicate your
  * decision by deleting the provisions above and replace them with the notice
  * and other provisions required by the GPL or the LGPL. If you do not delete
  * the provisions above, a recipient may use your version of this file under
  * the terms of any one of the MPL, the GPL or the LGPL.
  *
  * ***** END LICENSE BLOCK ***** */
 
-#ifdef DEBUG_bryner
-#define DEBUG_PAGE_CACHE
-#endif
-
 // Local Includes
 #include "nsSHEntry.h"
 #include "nsXPIDLString.h"
 #include "nsReadableUtils.h"
 #include "nsIDocShellLoadInfo.h"
 #include "nsIDocShellTreeItem.h"
 #include "nsIDocument.h"
 #include "nsIDOMDocument.h"
-#include "nsAutoPtr.h"
-#include "nsThreadUtils.h"
-#include "nsIWebNavigation.h"
 #include "nsISHistory.h"
 #include "nsISHistoryInternal.h"
 #include "nsDocShellEditorData.h"
-#include "nsIDocShell.h"
+#include "nsSHEntryShared.h"
+#include "nsILayoutHistoryState.h"
+#include "nsIContentViewer.h"
+#include "nsISupportsArray.h"
 
 namespace dom = mozilla::dom;
 
-// Hardcode this to time out unused content viewers after 30 minutes
-#define CONTENT_VIEWER_TIMEOUT_SECONDS 30*60
-
-typedef nsExpirationTracker<nsSHEntry,3> HistoryTrackerBase;
-class HistoryTracker : public HistoryTrackerBase {
-public:
-  // Expire cached contentviewers after 20-30 minutes in the cache.
-  HistoryTracker() : HistoryTrackerBase((CONTENT_VIEWER_TIMEOUT_SECONDS/2)*1000) {}
-  
-protected:
-  virtual void NotifyExpired(nsSHEntry* aObj) {
-    RemoveObject(aObj);
-    aObj->Expire();
-  }
-};
-
-static HistoryTracker *gHistoryTracker = nsnull;
 static PRUint32 gEntryID = 0;
-static PRUint64 gEntryDocIdentifier = 0;
-
-nsresult nsSHEntry::Startup()
-{
-  gHistoryTracker = new HistoryTracker();
-  return gHistoryTracker ? NS_OK : NS_ERROR_OUT_OF_MEMORY;
-}
-
-void nsSHEntry::Shutdown()
-{
-  delete gHistoryTracker;
-  gHistoryTracker = nsnull;
-}
-
-static void StopTrackingEntry(nsSHEntry *aEntry)
-{
-  if (aEntry->GetExpirationState()->IsTracked()) {
-    gHistoryTracker->RemoveObject(aEntry);
-  }
-}
 
 //*****************************************************************************
 //***    nsSHEntry: Object Management
 //*****************************************************************************
 
 
-nsSHEntry::nsSHEntry() 
+nsSHEntry::nsSHEntry()
   : mLoadType(0)
   , mID(gEntryID++)
-  , mDocIdentifier(gEntryDocIdentifier++)
   , mScrollPositionX(0)
   , mScrollPositionY(0)
   , mURIWasModified(PR_FALSE)
-  , mIsFrameNavigation(PR_FALSE)
-  , mSaveLayoutState(PR_TRUE)
-  , mExpired(PR_FALSE)
-  , mSticky(PR_TRUE)
-  , mDynamicallyCreated(PR_FALSE)
-  , mParent(nsnull)
-  , mViewerBounds(0, 0, 0, 0)
-  , mDocShellID(0)
-  , mLastTouched(0)
 {
+  mShared = new nsSHEntryShared();
 }
 
 nsSHEntry::nsSHEntry(const nsSHEntry &other)
-  : mURI(other.mURI)
+  : mShared(other.mShared)
+  , mURI(other.mURI)
   , mReferrerURI(other.mReferrerURI)
-  // XXX why not copy mDocument?
   , mTitle(other.mTitle)
   , mPostData(other.mPostData)
-  , mLayoutHistoryState(other.mLayoutHistoryState)
   , mLoadType(0)         // XXX why not copy?
   , mID(other.mID)
-  , mDocIdentifier(other.mDocIdentifier)
   , mScrollPositionX(0)  // XXX why not copy?
   , mScrollPositionY(0)  // XXX why not copy?
   , mURIWasModified(other.mURIWasModified)
-  , mIsFrameNavigation(other.mIsFrameNavigation)
-  , mSaveLayoutState(other.mSaveLayoutState)
-  , mExpired(other.mExpired)
-  , mSticky(PR_TRUE)
-  , mDynamicallyCreated(other.mDynamicallyCreated)
-  // XXX why not copy mContentType?
-  , mCacheKey(other.mCacheKey)
-  , mParent(other.mParent)
-  , mViewerBounds(0, 0, 0, 0)
-  , mOwner(other.mOwner)
-  , mDocShellID(other.mDocShellID)
   , mStateData(other.mStateData)
 {
 }
 
 static PRBool
 ClearParentPtr(nsISHEntry* aEntry, void* /* aData */)
 {
   if (aEntry) {
     aEntry->SetParent(nsnull);
   }
   return PR_TRUE;
 }
 
 nsSHEntry::~nsSHEntry()
 {
-  StopTrackingEntry(this);
-
-  // Since we never really remove kids from SHEntrys, we need to null
-  // out the mParent pointers on all our kids.
+  // Null out the mParent pointers on all our kids.
   mChildren.EnumerateForwards(ClearParentPtr, nsnull);
-  mChildren.Clear();
-
-  if (mContentViewer) {
-    // RemoveFromBFCacheSync is virtual, so call the nsSHEntry version
-    // explicitly
-    nsSHEntry::RemoveFromBFCacheSync();
-  }
-
-  mEditorData = nsnull;
-
-#ifdef DEBUG
-  // This is not happening as far as I can tell from breakpad as of early November 2007
-  nsExpirationTracker<nsSHEntry,3>::Iterator iterator(gHistoryTracker);
-  nsSHEntry* elem;
-  while ((elem = iterator.Next()) != nsnull) {
-    NS_ASSERTION(elem != this, "Found dead entry still in the tracker!");
-  }
-#endif
 }
 
 //*****************************************************************************
 //    nsSHEntry: nsISupports
 //*****************************************************************************
 
-NS_IMPL_ISUPPORTS5(nsSHEntry, nsISHContainer, nsISHEntry, nsIHistoryEntry,
-                   nsIMutationObserver, nsISHEntryInternal)
+NS_IMPL_ISUPPORTS4(nsSHEntry, nsISHContainer, nsISHEntry, nsIHistoryEntry,
+                   nsISHEntryInternal)
 
 //*****************************************************************************
 //    nsSHEntry: nsISHEntry
 //*****************************************************************************
 
 NS_IMETHODIMP nsSHEntry::SetScrollPosition(PRInt32 x, PRInt32 y)
 {
   mScrollPositionX = x;
@@ -246,45 +162,23 @@ NS_IMETHODIMP nsSHEntry::SetReferrerURI(
 {
   mReferrerURI = aReferrerURI;
   return NS_OK;
 }
 
 NS_IMETHODIMP
 nsSHEntry::SetContentViewer(nsIContentViewer *aViewer)
 {
-  NS_PRECONDITION(!aViewer || !mContentViewer, "SHEntry already contains viewer");
-
-  if (mContentViewer || !aViewer) {
-    DropPresentationState();
-  }
-
-  mContentViewer = aViewer;
-
-  if (mContentViewer) {
-    gHistoryTracker->AddObject(this);
-
-    nsCOMPtr<nsIDOMDocument> domDoc;
-    mContentViewer->GetDOMDocument(getter_AddRefs(domDoc));
-    // Store observed document in strong pointer in case it is removed from
-    // the contentviewer
-    mDocument = do_QueryInterface(domDoc);
-    if (mDocument) {
-      mDocument->SetBFCacheEntry(this);
-      mDocument->AddMutationObserver(this);
-    }
-  }
-
-  return NS_OK;
+  return mShared->SetContentViewer(aViewer);
 }
 
 NS_IMETHODIMP
 nsSHEntry::GetContentViewer(nsIContentViewer **aResult)
 {
-  *aResult = mContentViewer;
+  *aResult = mShared->mContentViewer;
   NS_IF_ADDREF(*aResult);
   return NS_OK;
 }
 
 NS_IMETHODIMP
 nsSHEntry::GetAnyContentViewer(nsISHEntry **aOwnerEntry,
                                nsIContentViewer **aResult)
 {
@@ -314,24 +208,24 @@ nsSHEntry::GetAnyContentViewer(nsISHEntr
     }
   }
   return NS_OK;
 }
 
 NS_IMETHODIMP
 nsSHEntry::SetSticky(PRBool aSticky)
 {
-  mSticky = aSticky;
+  mShared->mSticky = aSticky;
   return NS_OK;
 }
 
 NS_IMETHODIMP
 nsSHEntry::GetSticky(PRBool *aSticky)
 {
-  *aSticky = mSticky;
+  *aSticky = mShared->mSticky;
   return NS_OK;
 }
 
 NS_IMETHODIMP nsSHEntry::GetTitle(PRUnichar** aTitle)
 {
   // Check for empty title...
   if (mTitle.IsEmpty() && mURI) {
     // Default title is the URL.
@@ -360,26 +254,28 @@ NS_IMETHODIMP nsSHEntry::GetPostData(nsI
 NS_IMETHODIMP nsSHEntry::SetPostData(nsIInputStream* aPostData)
 {
   mPostData = aPostData;
   return NS_OK;
 }
 
 NS_IMETHODIMP nsSHEntry::GetLayoutHistoryState(nsILayoutHistoryState** aResult)
 {
-  *aResult = mLayoutHistoryState;
+  *aResult = mShared->mLayoutHistoryState;
   NS_IF_ADDREF(*aResult);
   return NS_OK;
 }
 
 NS_IMETHODIMP nsSHEntry::SetLayoutHistoryState(nsILayoutHistoryState* aState)
 {
-  mLayoutHistoryState = aState;
-  if (mLayoutHistoryState)
-    mLayoutHistoryState->SetScrollPositionOnly(!mSaveLayoutState);
+  mShared->mLayoutHistoryState = aState;
+  if (mShared->mLayoutHistoryState) {
+    mShared->mLayoutHistoryState->
+      SetScrollPositionOnly(!mShared->mSaveLayoutState);
+  }
 
   return NS_OK;
 }
 
 NS_IMETHODIMP nsSHEntry::GetLoadType(PRUint32 * aResult)
 {
   *aResult = mLoadType;
   return NS_OK;
@@ -398,209 +294,236 @@ NS_IMETHODIMP nsSHEntry::GetID(PRUint32 
 }
 
 NS_IMETHODIMP nsSHEntry::SetID(PRUint32  aID)
 {
   mID = aID;
   return NS_OK;
 }
 
-NS_IMETHODIMP nsSHEntry::GetDocIdentifier(PRUint64 * aResult)
-{
-  *aResult = mDocIdentifier;
-  return NS_OK;
-}
-
-NS_IMETHODIMP nsSHEntry::SetDocIdentifier(PRUint64 aDocIdentifier)
+nsSHEntryShared* nsSHEntry::GetSharedState()
 {
-  // This ensures that after a session restore, gEntryDocIdentifier is greater
-  // than all SHEntries' docIdentifiers, which ensures that we'll never repeat
-  // a doc identifier.
-  if (aDocIdentifier >= gEntryDocIdentifier)
-    gEntryDocIdentifier = aDocIdentifier + 1;
-
-  mDocIdentifier = aDocIdentifier;
-  return NS_OK;
-}
-
-NS_IMETHODIMP nsSHEntry::SetUniqueDocIdentifier()
-{
-  mDocIdentifier = gEntryDocIdentifier++;
-  return NS_OK;
+  return mShared;
 }
 
 NS_IMETHODIMP nsSHEntry::GetIsSubFrame(PRBool * aFlag)
 {
-  *aFlag = mIsFrameNavigation;
+  *aFlag = mShared->mIsFrameNavigation;
   return NS_OK;
 }
 
 NS_IMETHODIMP nsSHEntry::SetIsSubFrame(PRBool  aFlag)
 {
-  mIsFrameNavigation = aFlag;
+  mShared->mIsFrameNavigation = aFlag;
   return NS_OK;
 }
 
 NS_IMETHODIMP nsSHEntry::GetCacheKey(nsISupports** aResult)
 {
-  *aResult = mCacheKey;
+  *aResult = mShared->mCacheKey;
   NS_IF_ADDREF(*aResult);
   return NS_OK;
 }
 
 NS_IMETHODIMP nsSHEntry::SetCacheKey(nsISupports* aCacheKey)
 {
-  mCacheKey = aCacheKey;
+  mShared->mCacheKey = aCacheKey;
   return NS_OK;
 }
 
 NS_IMETHODIMP nsSHEntry::GetSaveLayoutStateFlag(PRBool * aFlag)
 {
-  *aFlag = mSaveLayoutState;
+  *aFlag = mShared->mSaveLayoutState;
   return NS_OK;
 }
 
 NS_IMETHODIMP nsSHEntry::SetSaveLayoutStateFlag(PRBool  aFlag)
 {
-  mSaveLayoutState = aFlag;
-  if (mLayoutHistoryState)
-    mLayoutHistoryState->SetScrollPositionOnly(!aFlag);
+  mShared->mSaveLayoutState = aFlag;
+  if (mShared->mLayoutHistoryState) {
+    mShared->mLayoutHistoryState->SetScrollPositionOnly(!aFlag);
+  }
 
   return NS_OK;
 }
 
 NS_IMETHODIMP nsSHEntry::GetExpirationStatus(PRBool * aFlag)
 {
-  *aFlag = mExpired;
+  *aFlag = mShared->mExpired;
   return NS_OK;
 }
 
 NS_IMETHODIMP nsSHEntry::SetExpirationStatus(PRBool  aFlag)
 {
-  mExpired = aFlag;
+  mShared->mExpired = aFlag;
   return NS_OK;
 }
 
 NS_IMETHODIMP nsSHEntry::GetContentType(nsACString& aContentType)
 {
-  aContentType = mContentType;
+  aContentType = mShared->mContentType;
   return NS_OK;
 }
 
 NS_IMETHODIMP nsSHEntry::SetContentType(const nsACString& aContentType)
 {
-  mContentType = aContentType;
+  mShared->mContentType = aContentType;
   return NS_OK;
 }
 
 NS_IMETHODIMP
 nsSHEntry::Create(nsIURI * aURI, const nsAString &aTitle,
                   nsIInputStream * aInputStream,
                   nsILayoutHistoryState * aLayoutHistoryState,
                   nsISupports * aCacheKey, const nsACString& aContentType,
                   nsISupports* aOwner,
                   PRUint64 aDocShellID, PRBool aDynamicCreation)
 {
   mURI = aURI;
   mTitle = aTitle;
   mPostData = aInputStream;
-  mCacheKey = aCacheKey;
-  mContentType = aContentType;
-  mOwner = aOwner;
-  mDocShellID = aDocShellID;
-  mDynamicallyCreated = aDynamicCreation;
 
   // Set the LoadType by default to loadHistory during creation
   mLoadType = (PRUint32) nsIDocShellLoadInfo::loadHistory;
 
+  mShared->mCacheKey = aCacheKey;
+  mShared->mContentType = aContentType;
+  mShared->mOwner = aOwner;
+  mShared->mDocShellID = aDocShellID;
+  mShared->mDynamicallyCreated = aDynamicCreation;
+
   // By default all entries are set false for subframe flag. 
   // nsDocShell::CloneAndReplace() which creates entries for
   // all subframe navigations, sets the flag to true.
-  mIsFrameNavigation = PR_FALSE;
+  mShared->mIsFrameNavigation = PR_FALSE;
 
   // By default we save LayoutHistoryState
-  mSaveLayoutState = PR_TRUE;
-  mLayoutHistoryState = aLayoutHistoryState;
+  mShared->mSaveLayoutState = PR_TRUE;
+  mShared->mLayoutHistoryState = aLayoutHistoryState;
 
   //By default the page is not expired
-  mExpired = PR_FALSE;
+  mShared->mExpired = PR_FALSE;
 
   return NS_OK;
 }
 
 NS_IMETHODIMP
 nsSHEntry::Clone(nsISHEntry ** aResult)
 {
   *aResult = new nsSHEntry(*this);
-  if (!*aResult)
-    return NS_ERROR_OUT_OF_MEMORY;
   NS_ADDREF(*aResult);
   return NS_OK;
 }
 
 NS_IMETHODIMP
 nsSHEntry::GetParent(nsISHEntry ** aResult)
 {
   NS_ENSURE_ARG_POINTER(aResult);
-  *aResult = mParent;
+  *aResult = mShared->mParent;
   NS_IF_ADDREF(*aResult);
   return NS_OK;
 }
 
 NS_IMETHODIMP
 nsSHEntry::SetParent(nsISHEntry * aParent)
 {
   /* parent not Addrefed on purpose to avoid cyclic reference
    * Null parent is OK
    *
    * XXX this method should not be scriptable if this is the case!!
    */
-  mParent = aParent;
+  mShared->mParent = aParent;
   return NS_OK;
 }
 
 NS_IMETHODIMP
 nsSHEntry::SetWindowState(nsISupports *aState)
 {
-  mWindowState = aState;
+  mShared->mWindowState = aState;
   return NS_OK;
 }
 
 NS_IMETHODIMP
 nsSHEntry::GetWindowState(nsISupports **aState)
 {
-  NS_IF_ADDREF(*aState = mWindowState);
+  NS_IF_ADDREF(*aState = mShared->mWindowState);
   return NS_OK;
 }
 
 NS_IMETHODIMP
 nsSHEntry::SetViewerBounds(const nsIntRect &aBounds)
 {
-  mViewerBounds = aBounds;
+  mShared->mViewerBounds = aBounds;
   return NS_OK;
 }
 
 NS_IMETHODIMP
 nsSHEntry::GetViewerBounds(nsIntRect &aBounds)
 {
-  aBounds = mViewerBounds;
+  aBounds = mShared->mViewerBounds;
   return NS_OK;
 }
 
 NS_IMETHODIMP
 nsSHEntry::GetOwner(nsISupports **aOwner)
 {
-  NS_IF_ADDREF(*aOwner = mOwner);
+  NS_IF_ADDREF(*aOwner = mShared->mOwner);
   return NS_OK;
 }
 
 NS_IMETHODIMP
 nsSHEntry::SetOwner(nsISupports *aOwner)
 {
-  mOwner = aOwner;
+  mShared->mOwner = aOwner;
+  return NS_OK;
+}
+
+NS_IMETHODIMP
+nsSHEntry::GetBFCacheEntry(nsIBFCacheEntry **aEntry)
+{
+  NS_ENSURE_ARG_POINTER(aEntry);
+  NS_IF_ADDREF(*aEntry = mShared);
+  return NS_OK;
+}
+
+PRBool
+nsSHEntry::HasBFCacheEntry(nsIBFCacheEntry *aEntry)
+{
+  return static_cast<nsIBFCacheEntry*>(mShared) == aEntry;
+}
+
+NS_IMETHODIMP
+nsSHEntry::AdoptBFCacheEntry(nsISHEntry *aEntry)
+{
+  nsCOMPtr<nsISHEntryInternal> shEntry = do_QueryInterface(aEntry);
+  NS_ENSURE_STATE(shEntry);
+
+  nsSHEntryShared *shared = shEntry->GetSharedState();
+  NS_ENSURE_STATE(shared);
+
+  mShared = shared;
+  return NS_OK;
+}
+
+NS_IMETHODIMP
+nsSHEntry::SharesDocumentWith(nsISHEntry *aEntry, PRBool *aOut)
+{
+  NS_ENSURE_ARG_POINTER(aOut);
+
+  nsCOMPtr<nsISHEntryInternal> internal = do_QueryInterface(aEntry); 
+  NS_ENSURE_STATE(internal);
+
+  *aOut = mShared == internal->GetSharedState();
+  return NS_OK;
+}
+
+NS_IMETHODIMP
+nsSHEntry::AbandonBFCacheEntry()
+{
+  mShared = nsSHEntryShared::Duplicate(mShared);
   return NS_OK;
 }
 
 //*****************************************************************************
 //    nsSHEntry: nsISHContainer
 //*****************************************************************************
 
 NS_IMETHODIMP 
@@ -748,266 +671,87 @@ nsSHEntry::GetChildAt(PRInt32 aIndex, ns
   }
   return NS_OK;
 }
 
 NS_IMETHODIMP
 nsSHEntry::AddChildShell(nsIDocShellTreeItem *aShell)
 {
   NS_ASSERTION(aShell, "Null child shell added to history entry");
-  mChildShells.AppendObject(aShell);
+  mShared->mChildShells.AppendObject(aShell);
   return NS_OK;
 }
 
 NS_IMETHODIMP
 nsSHEntry::ChildShellAt(PRInt32 aIndex, nsIDocShellTreeItem **aShell)
 {
-  NS_IF_ADDREF(*aShell = mChildShells.SafeObjectAt(aIndex));
+  NS_IF_ADDREF(*aShell = mShared->mChildShells.SafeObjectAt(aIndex));
   return NS_OK;
 }
 
 NS_IMETHODIMP
 nsSHEntry::ClearChildShells()
 {
-  mChildShells.Clear();
+  mShared->mChildShells.Clear();
   return NS_OK;
 }
 
 NS_IMETHODIMP
 nsSHEntry::GetRefreshURIList(nsISupportsArray **aList)
 {
-  NS_IF_ADDREF(*aList = mRefreshURIList);
+  NS_IF_ADDREF(*aList = mShared->mRefreshURIList);
   return NS_OK;
 }
 
 NS_IMETHODIMP
 nsSHEntry::SetRefreshURIList(nsISupportsArray *aList)
 {
-  mRefreshURIList = aList;
+  mShared->mRefreshURIList = aList;
   return NS_OK;
 }
 
 NS_IMETHODIMP
 nsSHEntry::SyncPresentationState()
 {
-  if (mContentViewer && mWindowState) {
-    // If we have a content viewer and a window state, we should be ok.
-    return NS_OK;
-  }
-
-  DropPresentationState();
-
-  return NS_OK;
-}
-
-void
-nsSHEntry::DropPresentationState()
-{
-  nsRefPtr<nsSHEntry> kungFuDeathGrip = this;
-
-  if (mDocument) {
-    mDocument->SetBFCacheEntry(nsnull);
-    mDocument->RemoveMutationObserver(this);
-    mDocument = nsnull;
-  }
-  if (mContentViewer)
-    mContentViewer->ClearHistoryEntry();
-
-  StopTrackingEntry(this);
-  mContentViewer = nsnull;
-  mSticky = PR_TRUE;
-  mWindowState = nsnull;
-  mViewerBounds.SetRect(0, 0, 0, 0);
-  mChildShells.Clear();
-  mRefreshURIList = nsnull;
-  mEditorData = nsnull;
-}
-
-void
-nsSHEntry::Expire()
-{
-  // This entry has timed out. If we still have a content viewer, we need to
-  // get it evicted.
-  if (!mContentViewer)
-    return;
-  nsCOMPtr<nsISupports> container;
-  mContentViewer->GetContainer(getter_AddRefs(container));
-  nsCOMPtr<nsIDocShellTreeItem> treeItem = do_QueryInterface(container);
-  if (!treeItem)
-    return;
-  // We need to find the root DocShell since only that object has an
-  // SHistory and we need the SHistory to evict content viewers
-  nsCOMPtr<nsIDocShellTreeItem> root;
-  treeItem->GetSameTypeRootTreeItem(getter_AddRefs(root));
-  nsCOMPtr<nsIWebNavigation> webNav = do_QueryInterface(root);
-  nsCOMPtr<nsISHistory> history;
-  webNav->GetSessionHistory(getter_AddRefs(history));
-  nsCOMPtr<nsISHistoryInternal> historyInt = do_QueryInterface(history);
-  if (!historyInt)
-    return;
-  historyInt->EvictExpiredContentViewerForEntry(this);
-}
-
-//*****************************************************************************
-//    nsSHEntry: nsIMutationObserver
-//*****************************************************************************
-
-void
-nsSHEntry::NodeWillBeDestroyed(const nsINode* aNode)
-{
-  NS_NOTREACHED("Document destroyed while we're holding a strong ref to it");
-}
-
-void
-nsSHEntry::CharacterDataWillChange(nsIDocument* aDocument,
-                                   nsIContent* aContent,
-                                   CharacterDataChangeInfo* aInfo)
-{
+  return mShared->SyncPresentationState();
 }
 
 void
-nsSHEntry::CharacterDataChanged(nsIDocument* aDocument,
-                                nsIContent* aContent,
-                                CharacterDataChangeInfo* aInfo)
-{
-  RemoveFromBFCacheAsync();
-}
-
-void
-nsSHEntry::AttributeWillChange(nsIDocument* aDocument,
-                               dom::Element* aContent,
-                               PRInt32 aNameSpaceID,
-                               nsIAtom* aAttribute,
-                               PRInt32 aModType)
-{
-}
-
-void
-nsSHEntry::AttributeChanged(nsIDocument* aDocument,
-                            dom::Element* aElement,
-                            PRInt32 aNameSpaceID,
-                            nsIAtom* aAttribute,
-                            PRInt32 aModType)
-{
-  RemoveFromBFCacheAsync();
-}
-
-void
-nsSHEntry::ContentAppended(nsIDocument* aDocument,
-                           nsIContent* aContainer,
-                           nsIContent* aFirstNewContent,
-                           PRInt32 /* unused */)
-{
-  RemoveFromBFCacheAsync();
-}
-
-void
-nsSHEntry::ContentInserted(nsIDocument* aDocument,
-                           nsIContent* aContainer,
-                           nsIContent* aChild,
-                           PRInt32 /* unused */)
-{
-  RemoveFromBFCacheAsync();
-}
-
-void
-nsSHEntry::ContentRemoved(nsIDocument* aDocument,
-                          nsIContent* aContainer,
-                          nsIContent* aChild,
-                          PRInt32 aIndexInContainer,
-                          nsIContent* aPreviousSibling)
-{
-  RemoveFromBFCacheAsync();
-}
-
-void
-nsSHEntry::ParentChainChanged(nsIContent *aContent)
-{
-}
-
-class DestroyViewerEvent : public nsRunnable
-{
-public:
-  DestroyViewerEvent(nsIContentViewer* aViewer, nsIDocument* aDocument)
-    : mViewer(aViewer),
-      mDocument(aDocument)
-  {}
-
-  NS_IMETHOD Run()
-  {
-    if (mViewer)
-      mViewer->Destroy();
-    return NS_OK;
-  }
-
-  nsCOMPtr<nsIContentViewer> mViewer;
-  nsCOMPtr<nsIDocument> mDocument;
-};
-
-void
 nsSHEntry::RemoveFromBFCacheSync()
 {
-  NS_ASSERTION(mContentViewer && mDocument,
-               "we're not in the bfcache!");
-
-  nsCOMPtr<nsIContentViewer> viewer = mContentViewer;
-  DropPresentationState();
-
-  // Warning! The call to DropPresentationState could have dropped the last
-  // reference to this nsSHEntry, so no accessing members beyond here.
-
-  if (viewer) {
-    viewer->Destroy();
-  }
+  mShared->RemoveFromBFCacheSync();
 }
 
 void
 nsSHEntry::RemoveFromBFCacheAsync()
 {
-  NS_ASSERTION(mContentViewer && mDocument,
-               "we're not in the bfcache!");
-
-  // Release the reference to the contentviewer asynchronously so that the
-  // document doesn't get nuked mid-mutation.
-
-  nsCOMPtr<nsIRunnable> evt =
-      new DestroyViewerEvent(mContentViewer, mDocument);
-  nsresult rv = NS_DispatchToCurrentThread(evt);
-  if (NS_FAILED(rv)) {
-    NS_WARNING("failed to dispatch DestroyViewerEvent");
-  }
-  else {
-    // Drop presentation. Also ensures that we don't post more then one
-    // PLEvent. Only do this if we succeeded in posting the event since
-    // otherwise the document could be torn down mid mutation causing crashes.
-    DropPresentationState();
-  }
-  // Warning! The call to DropPresentationState could have dropped the last
-  // reference to this nsSHEntry, so no accessing members beyond here.
+  mShared->RemoveFromBFCacheAsync();
 }
 
 nsDocShellEditorData*
 nsSHEntry::ForgetEditorData()
 {
-  return mEditorData.forget();
+  // XXX jlebar Check how this is used.
+  return mShared->mEditorData.forget();
 }
 
 void
 nsSHEntry::SetEditorData(nsDocShellEditorData* aData)
 {
-  NS_ASSERTION(!(aData && mEditorData),
+  NS_ASSERTION(!(aData && mShared->mEditorData),
                "We're going to overwrite an owning ref!");
-  if (mEditorData != aData)
-    mEditorData = aData;
+  if (mShared->mEditorData != aData) {
+    mShared->mEditorData = aData;
+  }
 }
 
 PRBool
 nsSHEntry::HasDetachedEditor()
 {
-  return mEditorData != nsnull;
+  return mShared->mEditorData != nsnull;
 }
 
 NS_IMETHODIMP
 nsSHEntry::GetStateData(nsIStructuredCloneContainer **aContainer)
 {
   NS_ENSURE_ARG_POINTER(aContainer);
   NS_IF_ADDREF(*aContainer = mStateData);
   return NS_OK;
@@ -1018,17 +762,17 @@ nsSHEntry::SetStateData(nsIStructuredClo
 {
   mStateData = aContainer;
   return NS_OK;
 }
 
 NS_IMETHODIMP
 nsSHEntry::IsDynamicallyAdded(PRBool* aAdded)
 {
-  *aAdded = mDynamicallyCreated;
+  *aAdded = mShared->mDynamicallyCreated;
   return NS_OK;
 }
 
 NS_IMETHODIMP
 nsSHEntry::HasDynamicallyAddedChild(PRBool* aAdded)
 {
   *aAdded = PR_FALSE;
   for (PRInt32 i = 0; i < mChildren.Count(); ++i) {
@@ -1041,34 +785,33 @@ nsSHEntry::HasDynamicallyAddedChild(PRBo
     }
   }
   return NS_OK;
 }
 
 NS_IMETHODIMP
 nsSHEntry::GetDocshellID(PRUint64* aID)
 {
-  *aID = mDocShellID;
+  *aID = mShared->mDocShellID;
   return NS_OK;
 }
 
 NS_IMETHODIMP
 nsSHEntry::SetDocshellID(PRUint64 aID)
 {
-  mDocShellID = aID;
+  mShared->mDocShellID = aID;
   return NS_OK;
 }
 
 
 NS_IMETHODIMP
 nsSHEntry::GetLastTouched(PRUint32 *aLastTouched)
 {
-  *aLastTouched = mLastTouched;
+  *aLastTouched = mShared->mLastTouched;
   return NS_OK;
 }
 
 NS_IMETHODIMP
 nsSHEntry::SetLastTouched(PRUint32 aLastTouched)
 {
-  mLastTouched = aLastTouched;
+  mShared->mLastTouched = aLastTouched;
   return NS_OK;
 }
-
--- a/docshell/shistory/src/nsSHEntry.h
+++ b/docshell/shistory/src/nsSHEntry.h
@@ -37,90 +37,62 @@
  *
  * ***** END LICENSE BLOCK ***** */
 
 #ifndef nsSHEntry_h
 #define nsSHEntry_h
 
 // Helper Classes
 #include "nsCOMPtr.h"
+#include "nsAutoPtr.h"
 #include "nsCOMArray.h"
 #include "nsString.h"
-#include "nsAutoPtr.h"
 
 // Interfaces needed
-#include "nsIContentViewer.h"
 #include "nsIInputStream.h"
-#include "nsILayoutHistoryState.h"
 #include "nsISHEntry.h"
 #include "nsISHContainer.h"
 #include "nsIURI.h"
-#include "nsIEnumerator.h"
 #include "nsIHistoryEntry.h"
-#include "nsRect.h"
-#include "nsISupportsArray.h"
-#include "nsIMutationObserver.h"
-#include "nsExpirationTracker.h"
-#include "nsDocShellEditorData.h"
+
+class nsSHEntryShared;
 
 class nsSHEntry : public nsISHEntry,
                   public nsISHContainer,
-                  public nsIMutationObserver,
                   public nsISHEntryInternal
 {
 public: 
   nsSHEntry();
   nsSHEntry(const nsSHEntry &other);
 
   NS_DECL_ISUPPORTS
   NS_DECL_NSIHISTORYENTRY
   NS_DECL_NSISHENTRY
   NS_DECL_NSISHENTRYINTERNAL
   NS_DECL_NSISHCONTAINER
-  NS_DECL_NSIMUTATIONOBSERVER
 
   void DropPresentationState();
 
-  void Expire();
-  
-  nsExpirationState *GetExpirationState() { return &mExpirationState; }
-  
   static nsresult Startup();
   static void Shutdown();
   
 private:
   ~nsSHEntry();
 
-  nsCOMPtr<nsIURI>                mURI;
-  nsCOMPtr<nsIURI>                mReferrerURI;
-  nsCOMPtr<nsIContentViewer>      mContentViewer;
-  nsCOMPtr<nsIDocument>           mDocument; // document currently being observed
-  nsString                        mTitle;
-  nsCOMPtr<nsIInputStream>        mPostData;
-  nsCOMPtr<nsILayoutHistoryState> mLayoutHistoryState;
-  nsCOMArray<nsISHEntry>          mChildren;
-  PRUint32                        mLoadType;
-  PRUint32                        mID;
-  PRInt64                         mDocIdentifier;
-  PRInt32                         mScrollPositionX;
-  PRInt32                         mScrollPositionY;
-  PRPackedBool                    mURIWasModified;
-  PRPackedBool                    mIsFrameNavigation;
-  PRPackedBool                    mSaveLayoutState;
-  PRPackedBool                    mExpired;
-  PRPackedBool                    mSticky;
-  PRPackedBool                    mDynamicallyCreated;
-  nsCString                       mContentType;
-  nsCOMPtr<nsISupports>           mCacheKey;
-  nsISHEntry *                    mParent;  // weak reference
-  nsCOMPtr<nsISupports>           mWindowState;
-  nsIntRect                       mViewerBounds;
-  nsCOMArray<nsIDocShellTreeItem> mChildShells;
-  nsCOMPtr<nsISupportsArray>      mRefreshURIList;
-  nsCOMPtr<nsISupports>           mOwner;
-  nsExpirationState               mExpirationState;
-  nsAutoPtr<nsDocShellEditorData> mEditorData;
-  PRUint64                        mDocShellID;
-  PRUint32                        mLastTouched;
+  // We share the state in here with other SHEntries which correspond to the
+  // same document.
+  nsRefPtr<nsSHEntryShared> mShared;
+
+  // See nsSHEntry.idl for comments on these members.
+  nsCOMPtr<nsIURI>         mURI;
+  nsCOMPtr<nsIURI>         mReferrerURI;
+  nsString                 mTitle;
+  nsCOMPtr<nsIInputStream> mPostData;
+  PRUint32                 mLoadType;
+  PRUint32                 mID;
+  PRInt32                  mScrollPositionX;
+  PRInt32                  mScrollPositionY;
+  nsCOMArray<nsISHEntry>   mChildren;
+  PRPackedBool             mURIWasModified;
   nsCOMPtr<nsIStructuredCloneContainer> mStateData;
 };
 
 #endif /* nsSHEntry_h */
new file mode 100644
--- /dev/null
+++ b/docshell/shistory/src/nsSHEntryShared.cpp
@@ -0,0 +1,348 @@
+#include "nsSHEntryShared.h"
+#include "nsISHistory.h"
+#include "nsISHistoryInternal.h"
+#include "nsIDocument.h"
+#include "nsIWebNavigation.h"
+#include "nsIContentViewer.h"
+#include "nsIDocShellTreeItem.h"
+#include "nsISupportsArray.h"
+#include "nsDocShellEditorData.h"
+#include "nsThreadUtils.h"
+#include "nsILayoutHistoryState.h"
+
+namespace dom = mozilla::dom;
+
+// Hardcode this to time out unused content viewers after 30 minutes
+// XXX jlebar shouldn't this be a pref?
+#define CONTENT_VIEWER_TIMEOUT_SECONDS (30*60)
+
+typedef nsExpirationTracker<nsSHEntryShared, 3> HistoryTrackerBase;
+class HistoryTracker : public HistoryTrackerBase {
+public:
+  // Expire cached contentviewers after 20-30 minutes in the cache.
+  HistoryTracker() 
+    : HistoryTrackerBase(1000 * CONTENT_VIEWER_TIMEOUT_SECONDS / 2)
+  {
+  }
+  
+protected:
+  virtual void NotifyExpired(nsSHEntryShared *aObj) {
+    RemoveObject(aObj);
+    aObj->Expire();
+  }
+};
+
+static HistoryTracker *gHistoryTracker = nsnull;
+
+void
+nsSHEntryShared::Startup()
+{
+  gHistoryTracker = new HistoryTracker();
+}
+
+void
+nsSHEntryShared::Shutdown()
+{
+  delete gHistoryTracker;
+  gHistoryTracker = nsnull;
+}
+
+nsSHEntryShared::nsSHEntryShared()
+  : mDocShellID(0)
+  , mParent(nsnull)
+  , mIsFrameNavigation(PR_FALSE)
+  , mSaveLayoutState(PR_TRUE)
+  , mSticky(PR_TRUE)
+  , mDynamicallyCreated(PR_FALSE)
+  , mLastTouched(0)
+  , mExpired(PR_FALSE)
+  , mViewerBounds(0, 0, 0, 0)
+{
+}
+
+nsSHEntryShared::~nsSHEntryShared()
+{
+  RemoveFromExpirationTracker();
+
+#ifdef DEBUG
+  // Check that we're not still on track to expire.  We shouldn't be, because
+  // we just removed ourselves!
+  nsExpirationTracker<nsSHEntryShared, 3>::Iterator
+    iterator(gHistoryTracker);
+
+  nsSHEntryShared *elem;
+  while ((elem = iterator.Next()) != nsnull) {
+    NS_ASSERTION(elem != this, "Found dead entry still in the tracker!");
+  }
+#endif
+
+  if (mContentViewer) {
+    RemoveFromBFCacheSync();
+  }
+}
+
+NS_IMPL_ISUPPORTS2(nsSHEntryShared, nsIBFCacheEntry, nsIMutationObserver)
+
+already_AddRefed<nsSHEntryShared>
+nsSHEntryShared::Duplicate(nsSHEntryShared *aEntry)
+{
+  nsRefPtr<nsSHEntryShared> newEntry = new nsSHEntryShared();
+
+  newEntry->mDocShellID = aEntry->mDocShellID;
+  newEntry->mChildShells.AppendObjects(aEntry->mChildShells);
+  newEntry->mOwner = aEntry->mOwner;
+  newEntry->mParent = aEntry->mParent;
+  newEntry->mContentType.Assign(aEntry->mContentType);
+  newEntry->mIsFrameNavigation = aEntry->mIsFrameNavigation;
+  newEntry->mSaveLayoutState = aEntry->mSaveLayoutState;
+  newEntry->mSticky = aEntry->mSticky;
+  newEntry->mDynamicallyCreated = aEntry->mDynamicallyCreated;
+  newEntry->mCacheKey = aEntry->mCacheKey;
+  newEntry->mLastTouched = aEntry->mLastTouched;
+
+  return newEntry.forget();
+}
+
+void nsSHEntryShared::RemoveFromExpirationTracker()
+{
+  if (GetExpirationState()->IsTracked()) {
+    gHistoryTracker->RemoveObject(this);
+  }
+}
+
+nsresult
+nsSHEntryShared::SyncPresentationState()
+{
+  if (mContentViewer && mWindowState) {
+    // If we have a content viewer and a window state, we should be ok.
+    return NS_OK;
+  }
+
+  DropPresentationState();
+
+  return NS_OK;
+}
+
+void
+nsSHEntryShared::DropPresentationState()
+{
+  nsRefPtr<nsSHEntryShared> kungFuDeathGrip = this;
+
+  if (mDocument) {
+    mDocument->SetBFCacheEntry(nsnull);
+    mDocument->RemoveMutationObserver(this);
+    mDocument = nsnull;
+  }
+  if (mContentViewer) {
+    mContentViewer->ClearHistoryEntry();
+  }
+
+  RemoveFromExpirationTracker();
+  mContentViewer = nsnull;
+  mSticky = PR_TRUE;
+  mWindowState = nsnull;
+  mViewerBounds.SetRect(0, 0, 0, 0);
+  mChildShells.Clear();
+  mRefreshURIList = nsnull;
+  mEditorData = nsnull;
+}
+
+void
+nsSHEntryShared::Expire()
+{
+  // This entry has timed out. If we still have a content viewer, we need to
+  // evict it.
+  if (!mContentViewer) {
+    return;
+  }
+  nsCOMPtr<nsISupports> container;
+  mContentViewer->GetContainer(getter_AddRefs(container));
+  nsCOMPtr<nsIDocShellTreeItem> treeItem = do_QueryInterface(container);
+  if (!treeItem) {
+    return;
+  }
+  // We need to find the root DocShell since only that object has an
+  // SHistory and we need the SHistory to evict content viewers
+  nsCOMPtr<nsIDocShellTreeItem> root;
+  treeItem->GetSameTypeRootTreeItem(getter_AddRefs(root));
+  nsCOMPtr<nsIWebNavigation> webNav = do_QueryInterface(root);
+  nsCOMPtr<nsISHistory> history;
+  webNav->GetSessionHistory(getter_AddRefs(history));
+  nsCOMPtr<nsISHistoryInternal> historyInt = do_QueryInterface(history);
+  if (!historyInt) {
+    return;
+  }
+  historyInt->EvictExpiredContentViewerForEntry(this);
+}
+
+nsresult
+nsSHEntryShared::SetContentViewer(nsIContentViewer *aViewer)
+{
+  NS_PRECONDITION(!aViewer || !mContentViewer,
+                  "SHEntryShared already contains viewer");
+
+  if (mContentViewer || !aViewer) {
+    DropPresentationState();
+  }
+
+  mContentViewer = aViewer;
+
+  if (mContentViewer) {
+    gHistoryTracker->AddObject(this);
+
+    nsCOMPtr<nsIDOMDocument> domDoc;
+    mContentViewer->GetDOMDocument(getter_AddRefs(domDoc));
+    // Store observed document in strong pointer in case it is removed from
+    // the contentviewer
+    mDocument = do_QueryInterface(domDoc);
+    if (mDocument) {
+      mDocument->SetBFCacheEntry(this);
+      mDocument->AddMutationObserver(this);
+    }
+  }
+
+  return NS_OK;
+}
+
+nsresult
+nsSHEntryShared::RemoveFromBFCacheSync()
+{
+  NS_ASSERTION(mContentViewer && mDocument,
+               "we're not in the bfcache!");
+
+  nsCOMPtr<nsIContentViewer> viewer = mContentViewer;
+  DropPresentationState();
+
+  // Warning! The call to DropPresentationState could have dropped the last
+  // reference to this object, so don't access members beyond here.
+
+  if (viewer) {
+    viewer->Destroy();
+  }
+
+  return NS_OK;
+}
+
+class DestroyViewerEvent : public nsRunnable
+{
+public:
+  DestroyViewerEvent(nsIContentViewer* aViewer, nsIDocument* aDocument)
+    : mViewer(aViewer),
+      mDocument(aDocument)
+  {}
+
+  NS_IMETHOD Run()
+  {
+    if (mViewer) {
+      mViewer->Destroy();
+    }
+    return NS_OK;
+  }
+
+  nsCOMPtr<nsIContentViewer> mViewer;
+  nsCOMPtr<nsIDocument> mDocument;
+};
+
+nsresult
+nsSHEntryShared::RemoveFromBFCacheAsync()
+{
+  NS_ASSERTION(mContentViewer && mDocument,
+               "we're not in the bfcache!");
+
+  // Release the reference to the contentviewer asynchronously so that the
+  // document doesn't get nuked mid-mutation.
+
+  nsCOMPtr<nsIRunnable> evt =
+    new DestroyViewerEvent(mContentViewer, mDocument);
+  nsresult rv = NS_DispatchToCurrentThread(evt);
+  if (NS_FAILED(rv)) {
+    NS_WARNING("failed to dispatch DestroyViewerEvent");
+  } else {
+    // Drop presentation. Only do this if we succeeded in posting the event
+    // since otherwise the document could be torn down mid-mutation, causing
+    // crashes.
+    DropPresentationState();
+  }
+
+  // Careful! The call to DropPresentationState could have dropped the last
+  // reference to this nsSHEntryShared, so don't access members beyond here.
+
+  return NS_OK;
+}
+
+//*****************************************************************************
+//    nsSHEntryShared: nsIMutationObserver
+//*****************************************************************************
+
+void
+nsSHEntryShared::NodeWillBeDestroyed(const nsINode* aNode)
+{
+  NS_NOTREACHED("Document destroyed while we're holding a strong ref to it");
+}
+
+void
+nsSHEntryShared::CharacterDataWillChange(nsIDocument* aDocument,
+                                         nsIContent* aContent,
+                                         CharacterDataChangeInfo* aInfo)
+{
+}
+
+void
+nsSHEntryShared::CharacterDataChanged(nsIDocument* aDocument,
+                                      nsIContent* aContent,
+                                      CharacterDataChangeInfo* aInfo)
+{
+  RemoveFromBFCacheAsync();
+}
+
+void
+nsSHEntryShared::AttributeWillChange(nsIDocument* aDocument,
+                                     dom::Element* aContent,
+                                     PRInt32 aNameSpaceID,
+                                     nsIAtom* aAttribute,
+                                     PRInt32 aModType)
+{
+}
+
+void
+nsSHEntryShared::AttributeChanged(nsIDocument* aDocument,
+                                  dom::Element* aElement,
+                                  PRInt32 aNameSpaceID,
+                                  nsIAtom* aAttribute,
+                                  PRInt32 aModType)
+{
+  RemoveFromBFCacheAsync();
+}
+
+void
+nsSHEntryShared::ContentAppended(nsIDocument* aDocument,
+                                 nsIContent* aContainer,
+                                 nsIContent* aFirstNewContent,
+                                 PRInt32 /* unused */)
+{
+  RemoveFromBFCacheAsync();
+}
+
+void
+nsSHEntryShared::ContentInserted(nsIDocument* aDocument,
+                                 nsIContent* aContainer,
+                                 nsIContent* aChild,
+                                 PRInt32 /* unused */)
+{
+  RemoveFromBFCacheAsync();
+}
+
+void
+nsSHEntryShared::ContentRemoved(nsIDocument* aDocument,
+                                nsIContent* aContainer,
+                                nsIContent* aChild,
+                                PRInt32 aIndexInContainer,
+                                nsIContent* aPreviousSibling)
+{
+  RemoveFromBFCacheAsync();
+}
+
+void
+nsSHEntryShared::ParentChainChanged(nsIContent *aContent)
+{
+}
new file mode 100644
--- /dev/null
+++ b/docshell/shistory/src/nsSHEntryShared.h
@@ -0,0 +1,88 @@
+
+#ifndef nsSHEntryShared_h__
+#define nsSHEntryShared_h__
+
+#include "nsCOMPtr.h"
+#include "nsAutoPtr.h"
+#include "nsCOMArray.h"
+#include "nsIBFCacheEntry.h"
+#include "nsIMutationObserver.h"
+#include "nsExpirationTracker.h"
+#include "nsRect.h"
+
+class nsSHEntry;
+class nsISHEntry;
+class nsIDocument;
+class nsIContentViewer;
+class nsIDocShellTreeItem;
+class nsILayoutHistoryState;
+class nsISupportsArray;
+class nsDocShellEditorData;
+
+// A document may have multiple SHEntries, either due to hash navigations or
+// calls to history.pushState.  SHEntries corresponding to the same document
+// share many members; in particular, they share state related to the
+// back/forward cache.
+//
+// nsSHEntryShared is the vehicle for this sharing.
+class nsSHEntryShared : public nsIBFCacheEntry,
+                        public nsIMutationObserver
+{
+  public:
+    static void Startup();
+    static void Shutdown();
+
+    nsSHEntryShared();
+    ~nsSHEntryShared();
+
+    NS_DECL_ISUPPORTS
+    NS_DECL_NSIMUTATIONOBSERVER
+    NS_DECL_NSIBFCACHEENTRY
+
+  private:
+    friend class nsSHEntry;
+
+    friend class HistoryTracker;
+    friend class nsExpirationTracker<nsSHEntryShared, 3>;
+    nsExpirationState *GetExpirationState() { return &mExpirationState; }
+
+    static already_AddRefed<nsSHEntryShared> Duplicate(nsSHEntryShared *aEntry);
+    void SetDocIdentifier(PRUint64 aDocIdentifier);
+
+    void RemoveFromExpirationTracker();
+    void Expire();
+    nsresult SyncPresentationState();
+    void DropPresentationState();
+
+    nsresult SetContentViewer(nsIContentViewer *aViewer);
+
+    // See nsISHEntry.idl for an explanation of these members.
+
+    // These members are copied by nsSHEntryShared::Duplicate().  If you add a
+    // member here, be sure to update the Duplicate() implementation.
+    PRUint64                        mDocShellID;
+    nsCOMArray<nsIDocShellTreeItem> mChildShells;
+    nsCOMPtr<nsISupports>           mOwner;
+    nsISHEntry*                     mParent;
+    nsCString                       mContentType;
+    PRPackedBool                    mIsFrameNavigation;
+    PRPackedBool                    mSaveLayoutState;
+    PRPackedBool                    mSticky;
+    PRPackedBool                    mDynamicallyCreated;
+    nsCOMPtr<nsISupports>           mCacheKey;
+    PRUint32                        mLastTouched;
+
+    // These members aren't copied by nsSHEntryShared::Duplicate() because
+    // they're specific to a particular content viewer.
+    nsCOMPtr<nsIContentViewer>      mContentViewer;
+    nsCOMPtr<nsIDocument>           mDocument;
+    nsCOMPtr<nsILayoutHistoryState> mLayoutHistoryState;
+    PRPackedBool                    mExpired;
+    nsCOMPtr<nsISupports>           mWindowState;
+    nsIntRect                       mViewerBounds;
+    nsCOMPtr<nsISupportsArray>      mRefreshURIList;
+    nsExpirationState               mExpirationState;
+    nsAutoPtr<nsDocShellEditorData> mEditorData;
+};
+
+#endif
--- a/docshell/shistory/src/nsSHistory.cpp
+++ b/docshell/shistory/src/nsSHistory.cpp
@@ -1076,31 +1076,34 @@ nsSHistory::EvictGlobalContentViewer()
       }
     } else {
       // couldn't find a content viewer to evict, so we are done
       shouldTryEviction = PR_FALSE;
     }
   }  // while shouldTryEviction
 }
 
-NS_IMETHODIMP
-nsSHistory::EvictExpiredContentViewerForEntry(nsISHEntry *aEntry)
+nsresult
+nsSHistory::EvictExpiredContentViewerForEntry(nsIBFCacheEntry *aEntry)
 {
   PRInt32 startIndex = NS_MAX(0, mIndex - gHistoryMaxViewers);
   PRInt32 endIndex = NS_MIN(mLength - 1,
                             mIndex + gHistoryMaxViewers);
   nsCOMPtr<nsISHTransaction> trans;
   GetTransactionAtIndex(startIndex, getter_AddRefs(trans));
 
   PRInt32 i;
   for (i = startIndex; trans && i <= endIndex; ++i) {
     nsCOMPtr<nsISHEntry> entry;
     trans->GetSHEntry(getter_AddRefs(entry));
-    if (entry == aEntry)
+
+    // Does entry have the same BFCacheEntry as the argument to this method?
+    if (entry->HasBFCacheEntry(aEntry)) {
       break;
+    }
 
     nsISHTransaction *temp = trans;
     temp->GetNext(getter_AddRefs(trans));
   }
   if (i > endIndex)
     return NS_OK;
   
   NS_ASSERTION(i != mIndex, "How did the current session entry expire?");
@@ -1112,17 +1115,17 @@ nsSHistory::EvictExpiredContentViewerFor
   // that have the expired entry between them and the current entry). Those
   // other entries should have timed out already, actually, but this is just
   // to be on the safe side.
   if (i < mIndex) {
     EvictContentViewersInRange(startIndex, i + 1);
   } else {
     EvictContentViewersInRange(i, endIndex + 1);
   }
-  
+
   return NS_OK;
 }
 
 // Evicts all content viewers in all history objects.  This is very
 // inefficient, because it requires a linear search through all SHistory
 // objects for each viewer to be evicted.  However, this method is called
 // infrequently -- only when the disk or memory cache is cleared.
 
--- a/docshell/test/Makefile.in
+++ b/docshell/test/Makefile.in
@@ -114,16 +114,17 @@ include $(topsrcdir)/config/rules.mk
 		test_bug570341.html \
 		bug570341_recordevents.html \
 		test_bug668513.html \
 		bug668513_redirect.html \
 		bug668513_redirect.html^headers^ \
 		test_bug669671.html \
 		file_bug669671.sjs \
 		test_bug675587.html \
+		test_bfcache_plus_hash.html \
 		$(NULL)
 
 ifeq ($(MOZ_WIDGET_TOOLKIT),cocoa)
 _TEST_FILES += \
 		test_bug511449.html \
 		file_bug511449.html \
 		$(NULL)
 endif
new file mode 100644
--- /dev/null
+++ b/docshell/test/test_bfcache_plus_hash.html
@@ -0,0 +1,121 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=646641
+-->
+<head>
+  <title>Test for Bug 646641</title>
+  <script type="application/javascript" src="/MochiKit/packed.js"></script>
+  <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+  <script type="text/javascript" src="/tests/SimpleTest/WindowSnapshot.js"></script>
+  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=646641">Mozilla Bug 646641</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+  
+</div>
+<pre id="test">
+<script type="application/javascript;version=1.7">
+
+/** Test for Bug 646641 **/
+
+/* 
+ * In a popup (because navigating the main frame confuses Mochitest), do the
+ * following:
+ *
+ *  * Call history.pushState().
+ *  * Navigate to a new page.
+ *  * Go back two history entries.
+ *
+ * Check that we go back, we retrieve the document from bfcache.
+ */
+
+SimpleTest.waitForExplicitFinish();
+
+function debug(msg) {
+  // Wrap dump so we can turn debug messages on and off easily.
+  dump(msg + '\n');
+}
+
+var expectedLoadNum = -1;
+function childLoad(n) {
+  if (n == expectedLoadNum) {
+    debug('Got load ' + n);
+    expectedLoadNum = -1;
+
+    // Spin the event loop before calling gGen.next() so the generator runs
+    // outside the onload handler.  This prevents us from encountering all
+    // sorts of docshell quirks.
+    //
+    // (I don't know why I need to wrap gGen.next() in a function, but it
+    // throws an error otherwise.)
+    setTimeout(function() { gGen.next() }, 0);
+  }
+  else {
+    debug('Got unexpected load ' + n);
+    ok(false, 'Got unexpected load ' + n);
+  }
+}
+
+var expectedPageshowNum = -1;
+function childPageshow(n) {
+  if (n == expectedPageshowNum) {
+    debug('Got expected pageshow ' + n);
+    expectedPageshowNum = -1;
+    ok(true, 'Got expected pageshow ' + n);
+    setTimeout(function() { gGen.next() }, 0);
+  }
+  else {
+    debug('Got pageshow ' + n);
+  }
+
+  // Since a pageshow comes along with an onload, don't fail the test if we get
+  // an unexpected pageshow.
+}
+
+function waitForLoad(n) {
+  debug('Waiting for load ' + n);
+  expectedLoadNum = n;
+}
+
+function waitForShow(n) {
+  debug('Waiting for show ' + n);
+  expectedPageshowNum = n;
+}
+
+function test() {
+  var popup = window.open('data:text/html,' +
+                          '<html><body onload="opener.childLoad(1)" ' +
+                                      'onpageshow="opener.childPageshow(1)">' +
+                                'Popup 1' +
+                                '</body></html>');
+  waitForLoad(1);
+  yield;
+
+  popup.history.pushState('', '', '');
+
+  popup.location = 'data:text/html,<html><body onload="opener.childLoad(2)">Popup 2</body></html>';
+  waitForLoad(2);
+  yield;
+
+  // Now go back 2.  The first page should be retrieved from bfcache.
+  popup.history.go(-2);
+  waitForShow(1);
+  yield;
+
+  popup.close();
+  SimpleTest.finish();
+  
+  // Yield once more so we don't throw a StopIteration exception.
+  yield;
+}
+
+var gGen = test();
+gGen.next();
+
+</script>
+</pre>
+</body>
+</html>
--- a/dom/indexedDB/IndexedDatabaseManager.cpp
+++ b/dom/indexedDB/IndexedDatabaseManager.cpp
@@ -168,22 +168,19 @@ public:
       nsRefPtr<IDBDatabase>& database = mWaitingDatabases[index];
 
       if (database->IsClosed()) {
         continue;
       }
 
       // First check if the document the IDBDatabase is part of is bfcached
       nsCOMPtr<nsIDocument> ownerDoc = database->GetOwnerDocument();
-      nsISHEntry* shEntry;
-      if (ownerDoc && (shEntry = ownerDoc->GetBFCacheEntry())) {
-        nsCOMPtr<nsISHEntryInternal> sheInternal = do_QueryInterface(shEntry);
-        if (sheInternal) {
-          sheInternal->RemoveFromBFCacheSync();
-        }
+      nsIBFCacheEntry* bfCacheEntry;
+      if (ownerDoc && (bfCacheEntry = ownerDoc->GetBFCacheEntry())) {
+        bfCacheEntry->RemoveFromBFCacheSync();
         NS_ASSERTION(database->IsClosed(),
                      "Kicking doc out of bfcache should have closed database");
         continue;
       }
 
       // Otherwise fire a versionchange event.
       nsCOMPtr<nsIDOMEvent> event(IDBVersionChangeEvent::Create(mVersion));
       NS_ENSURE_TRUE(event, NS_ERROR_FAILURE);