Backed out changeset e62c93a8c93b (bug 1373672) for bustage at nsSessionStoreUtils.cpp:21: bad implicit conversion constructor for 'DynamicFrameEventFilter, and e.g. eslint failures. r=backout on a CLOSED TREE
authorSebastian Hengst <archaeopteryx@coole-files.de>
Tue, 01 Aug 2017 12:39:21 +0200
changeset 420915 0c274a6f7b7b9b41b4b29e34037f4f60244f8d05
parent 420914 6da77209f66bc33af8a22905bb75007ef59e0abb
child 420916 4f8e0cb21016059dd19fb4b706e960ec88c64023
push id7566
push usermtabara@mozilla.com
push dateWed, 02 Aug 2017 08:25:16 +0000
treeherdermozilla-beta@86913f512c3c [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersbackout
bugs1373672
milestone56.0a1
backs oute62c93a8c93b83f21e417fc61ee4f163ea48cb8f
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
Backed out changeset e62c93a8c93b (bug 1373672) for bustage at nsSessionStoreUtils.cpp:21: bad implicit conversion constructor for 'DynamicFrameEventFilter, and e.g. eslint failures. r=backout on a CLOSED TREE
browser/components/build/moz.build
browser/components/build/nsModule.cpp
browser/components/sessionstore/ContentRestore.jsm
browser/components/sessionstore/FrameTree.jsm
browser/components/sessionstore/SessionStorage.jsm
browser/components/sessionstore/content/content-sessionStore.js
browser/components/sessionstore/moz.build
browser/components/sessionstore/nsISessionStoreUtils.idl
browser/components/sessionstore/nsSessionStoreUtils.cpp
browser/components/sessionstore/nsSessionStoreUtils.h
browser/components/sessionstore/test/browser.ini
browser/components/sessionstore/test/browser_formdata_sample.html
browser/components/sessionstore/test/browser_frametree.js
browser/components/sessionstore/test/browser_frametree_sample_iframes.html
browser/components/sessionstore/test/browser_sessionHistory.js
browser/components/sessionstore/test/browser_sessionStorage.html
browser/components/sessionstore/test/content.js
dom/base/nsFrameLoader.cpp
toolkit/modules/sessionstore/FormData.jsm
toolkit/modules/sessionstore/ScrollPosition.jsm
--- a/browser/components/build/moz.build
+++ b/browser/components/build/moz.build
@@ -18,11 +18,10 @@ SOURCES += [
 Library('browsercomps')
 FINAL_LIBRARY = 'xul'
 
 LOCAL_INCLUDES += [
     '../about',
     '../dirprovider',
     '../feeds',
     '../migration',
-    '../sessionstore',
     '../shell',
 ]
--- a/browser/components/build/nsModule.cpp
+++ b/browser/components/build/nsModule.cpp
@@ -19,17 +19,16 @@
 #if defined(XP_WIN)
 #include "nsIEHistoryEnumerator.h"
 #endif
 
 #include "rdf.h"
 #include "nsFeedSniffer.h"
 #include "AboutRedirector.h"
 #include "nsIAboutModule.h"
-#include "nsSessionStoreUtils.h"
 
 #include "nsNetCID.h"
 
 using namespace mozilla::browser;
 
 /////////////////////////////////////////////////////////////////////////////
 
 NS_GENERIC_FACTORY_CONSTRUCTOR(DirectoryProvider)
@@ -56,46 +55,41 @@ NS_DEFINE_NAMED_CID(NS_SHELLSERVICE_CID)
 NS_DEFINE_NAMED_CID(NS_FEEDSNIFFER_CID);
 NS_DEFINE_NAMED_CID(NS_BROWSER_ABOUT_REDIRECTOR_CID);
 #if defined(XP_WIN)
 NS_DEFINE_NAMED_CID(NS_WINIEHISTORYENUMERATOR_CID);
 #elif defined(XP_MACOSX)
 NS_DEFINE_NAMED_CID(NS_SHELLSERVICE_CID);
 #endif
 
-NS_GENERIC_FACTORY_CONSTRUCTOR(nsSessionStoreUtils)
-NS_DEFINE_NAMED_CID(NS_SESSIONSTOREUTILS_CID);
-
 static const mozilla::Module::CIDEntry kBrowserCIDs[] = {
     { &kNS_BROWSERDIRECTORYPROVIDER_CID, false, nullptr, DirectoryProviderConstructor },
 #if defined(XP_WIN)
     { &kNS_SHELLSERVICE_CID, false, nullptr, nsWindowsShellServiceConstructor },
 #elif defined(MOZ_WIDGET_GTK)
     { &kNS_SHELLSERVICE_CID, false, nullptr, nsGNOMEShellServiceConstructor },
 #endif
     { &kNS_FEEDSNIFFER_CID, false, nullptr, nsFeedSnifferConstructor },
     { &kNS_BROWSER_ABOUT_REDIRECTOR_CID, false, nullptr, AboutRedirector::Create },
 #if defined(XP_WIN)
     { &kNS_WINIEHISTORYENUMERATOR_CID, false, nullptr, nsIEHistoryEnumeratorConstructor },
 #elif defined(XP_MACOSX)
     { &kNS_SHELLSERVICE_CID, false, nullptr, nsMacShellServiceConstructor },
 #endif
-    { &kNS_SESSIONSTOREUTILS_CID, false, nullptr, nsSessionStoreUtilsConstructor },
     { nullptr }
 };
 
 static const mozilla::Module::ContractIDEntry kBrowserContracts[] = {
     { NS_BROWSERDIRECTORYPROVIDER_CONTRACTID, &kNS_BROWSERDIRECTORYPROVIDER_CID },
 #if defined(XP_WIN)
     { NS_SHELLSERVICE_CONTRACTID, &kNS_SHELLSERVICE_CID },
 #elif defined(MOZ_WIDGET_GTK)
     { NS_SHELLSERVICE_CONTRACTID, &kNS_SHELLSERVICE_CID },
 #endif
     { NS_FEEDSNIFFER_CONTRACTID, &kNS_FEEDSNIFFER_CID },
-    { NS_SESSIONSTOREUTILS_CONTRACTID, &kNS_SESSIONSTOREUTILS_CID },
     { NS_ABOUT_MODULE_CONTRACTID_PREFIX "blocked", &kNS_BROWSER_ABOUT_REDIRECTOR_CID },
     { NS_ABOUT_MODULE_CONTRACTID_PREFIX "certerror", &kNS_BROWSER_ABOUT_REDIRECTOR_CID },
     { NS_ABOUT_MODULE_CONTRACTID_PREFIX "socialerror", &kNS_BROWSER_ABOUT_REDIRECTOR_CID },
     { NS_ABOUT_MODULE_CONTRACTID_PREFIX "providerdirectory", &kNS_BROWSER_ABOUT_REDIRECTOR_CID },
     { NS_ABOUT_MODULE_CONTRACTID_PREFIX "tabcrashed", &kNS_BROWSER_ABOUT_REDIRECTOR_CID },
     { NS_ABOUT_MODULE_CONTRACTID_PREFIX "feeds", &kNS_BROWSER_ABOUT_REDIRECTOR_CID },
     { NS_ABOUT_MODULE_CONTRACTID_PREFIX "privatebrowsing", &kNS_BROWSER_ABOUT_REDIRECTOR_CID },
     { NS_ABOUT_MODULE_CONTRACTID_PREFIX "rights", &kNS_BROWSER_ABOUT_REDIRECTOR_CID },
--- a/browser/components/sessionstore/ContentRestore.jsm
+++ b/browser/components/sessionstore/ContentRestore.jsm
@@ -2,17 +2,16 @@
 * 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/. */
 
 "use strict";
 
 this.EXPORTED_SYMBOLS = ["ContentRestore"];
 
 const Cu = Components.utils;
-const Cc = Components.classes;
 const Ci = Components.interfaces;
 
 Cu.import("resource://gre/modules/XPCOMUtils.jsm", this);
 Cu.import("resource://gre/modules/Services.jsm", this);
 
 XPCOMUtils.defineLazyModuleGetter(this, "DocShellCapabilities",
   "resource:///modules/sessionstore/DocShellCapabilities.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "FormData",
@@ -21,43 +20,16 @@ XPCOMUtils.defineLazyModuleGetter(this, 
   "resource://gre/modules/ScrollPosition.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "SessionHistory",
   "resource://gre/modules/sessionstore/SessionHistory.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "SessionStorage",
   "resource:///modules/sessionstore/SessionStorage.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "Utils",
   "resource://gre/modules/sessionstore/Utils.jsm");
 
-const ssu = Cc["@mozilla.org/browser/sessionstore/utils;1"]
-              .getService(Ci.nsISessionStoreUtils);
-
-/**
- * Restores frame tree |data|, starting at the given root |frame|. As the
- * function recurses into descendant frames it will call cb(frame, data) for
- * each frame it encounters, starting with the given root.
- */
-function restoreFrameTreeData(frame, data, cb) {
-  // Restore data for the root frame.
-  // The callback can abort by returning false.
-  if (cb(frame, data) === false) {
-    return;
-  }
-
-  if (!data.hasOwnProperty("children")) {
-    return;
-  }
-
-  // Recurse into child frames.
-  ssu.forEachNonDynamicChildFrame(frame, (subframe, index) => {
-    if (data.children[index]) {
-      restoreFrameTreeData(subframe, data.children[index], cb);
-    }
-  });
-}
-
 /**
  * This module implements the content side of session restoration. The chrome
  * side is handled by SessionStore.jsm. The functions in this module are called
  * by content-sessionStore.js based on messages received from SessionStore.jsm
  * (or, in one case, based on a "load" event). Each tab has its own
  * ContentRestore instance, constructed by content-sessionStore.js.
  *
  * In a typical restore, content-sessionStore.js will call the following based
@@ -317,30 +289,18 @@ ContentRestoreInternal.prototype = {
       return;
     }
     let {formdata, scrollPositions} = this._restoringDocument;
     this._restoringDocument = null;
 
     let window = this.docShell.QueryInterface(Ci.nsIInterfaceRequestor)
                                .getInterface(Ci.nsIDOMWindow);
 
-    // Restore form data.
-    restoreFrameTreeData(window, formdata, (frame, data) => {
-      // restore() will return false, and thus abort restoration for the
-      // current |frame| and its descendants, if |data.url| is given but
-      // doesn't match the loaded document's URL.
-      return FormData.restore(frame, data);
-    });
-
-    // Restore scroll data.
-    restoreFrameTreeData(window, scrollPositions, (frame, data) => {
-      if (data.scroll) {
-        ScrollPosition.restore(frame, data.scroll);
-      }
-    });
+    FormData.restoreTree(window, formdata);
+    ScrollPosition.restoreTree(window, scrollPositions);
   },
 
   /**
    * Cancel an ongoing restore. This function can be called any time between
    * restoreHistory and restoreDocument.
    *
    * This function is called externally (if a restore is canceled) and
    * internally (when the loads for a restore have finished). In the latter
new file mode 100644
--- /dev/null
+++ b/browser/components/sessionstore/FrameTree.jsm
@@ -0,0 +1,248 @@
+/* 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/. */
+
+"use strict";
+
+this.EXPORTED_SYMBOLS = ["FrameTree"];
+
+const Cu = Components.utils;
+const Ci = Components.interfaces;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm", this);
+
+const EXPORTED_METHODS = ["addObserver", "contains", "map", "forEach"];
+
+/**
+ * A FrameTree represents all frames that were reachable when the document
+ * was loaded. We use this information to ignore frames when collecting
+ * sessionstore data as we can't currently restore anything for frames that
+ * have been created dynamically after or at the load event.
+ *
+ * @constructor
+ */
+function FrameTree(chromeGlobal) {
+  let internal = new FrameTreeInternal(chromeGlobal);
+  let external = {};
+
+  for (let method of EXPORTED_METHODS) {
+    external[method] = internal[method].bind(internal);
+  }
+
+  return Object.freeze(external);
+}
+
+/**
+ * The internal frame tree API that the public one points to.
+ *
+ * @constructor
+ */
+function FrameTreeInternal(chromeGlobal) {
+  // A WeakMap that uses frames (DOMWindows) as keys and their initial indices
+  // in their parents' child lists as values. Suppose we have a root frame with
+  // three subframes i.e. a page with three iframes. The WeakMap would have
+  // four entries and look as follows:
+  //
+  // root -> 0
+  // subframe1 -> 0
+  // subframe2 -> 1
+  // subframe3 -> 2
+  //
+  // Should one of the subframes disappear we will stop collecting data for it
+  // as |this._frames.has(frame) == false|. All other subframes will maintain
+  // their initial indices to ensure we can restore frame data appropriately.
+  this._frames = new WeakMap();
+
+  // The Set of observers that will be notified when the frame changes.
+  this._observers = new Set();
+
+  // The chrome global we use to retrieve the current DOMWindow.
+  this._chromeGlobal = chromeGlobal;
+
+  // Register a web progress listener to be notified about new page loads.
+  let docShell = chromeGlobal.docShell;
+  let ifreq = docShell.QueryInterface(Ci.nsIInterfaceRequestor);
+  let webProgress = ifreq.getInterface(Ci.nsIWebProgress);
+  webProgress.addProgressListener(this, Ci.nsIWebProgress.NOTIFY_STATE_DOCUMENT);
+}
+
+FrameTreeInternal.prototype = {
+
+  // Returns the docShell's current global.
+  get content() {
+    return this._chromeGlobal.content;
+  },
+
+  /**
+   * Adds a given observer |obs| to the set of observers that will be notified
+   * when the frame tree is reset (when a new document starts loading) or
+   * recollected (when a document finishes loading).
+   *
+   * @param obs (object)
+   */
+  addObserver(obs) {
+    this._observers.add(obs);
+  },
+
+  /**
+   * Notifies all observers that implement the given |method|.
+   *
+   * @param method (string)
+   */
+  notifyObservers(method) {
+    for (let obs of this._observers) {
+      if (obs.hasOwnProperty(method)) {
+        obs[method]();
+      }
+    }
+  },
+
+  /**
+   * Checks whether a given |frame| is contained in the collected frame tree.
+   * If it is not, this indicates that we should not collect data for it.
+   *
+   * @param frame (nsIDOMWindow)
+   * @return bool
+   */
+  contains(frame) {
+    return this._frames.has(frame);
+  },
+
+  /**
+   * Recursively applies the given function |cb| to the stored frame tree. Use
+   * this method to collect sessionstore data for all reachable frames stored
+   * in the frame tree.
+   *
+   * If a given function |cb| returns a value, it must be an object. It may
+   * however return "null" to indicate that there is no data to be stored for
+   * the given frame.
+   *
+   * The object returned by |cb| cannot have any property named "children" as
+   * that is used to store information about subframes in the tree returned
+   * by |map()| and might be overridden.
+   *
+   * @param cb (function)
+   * @return object
+   */
+  map(cb) {
+    let frames = this._frames;
+
+    function walk(frame) {
+      let obj = cb(frame) || {};
+
+      if (frames.has(frame)) {
+        let children = [];
+
+        Array.forEach(frame.frames, subframe => {
+          // Don't collect any data if the frame is not contained in the
+          // initial frame tree. It's a dynamic frame added later.
+          if (!frames.has(subframe)) {
+            return;
+          }
+
+          // Retrieve the frame's original position in its parent's child list.
+          let index = frames.get(subframe);
+
+          // Recursively collect data for the current subframe.
+          let result = walk(subframe, cb);
+          if (result && Object.keys(result).length) {
+            children[index] = result;
+          }
+        });
+
+        if (children.length) {
+          obj.children = children;
+        }
+      }
+
+      return Object.keys(obj).length ? obj : null;
+    }
+
+    return walk(this.content);
+  },
+
+  /**
+   * Applies the given function |cb| to all frames stored in the tree. Use this
+   * method if |map()| doesn't suit your needs and you want more control over
+   * how data is collected.
+   *
+   * @param cb (function)
+   *        This callback receives the current frame as the only argument.
+   */
+  forEach(cb) {
+    let frames = this._frames;
+
+    function walk(frame) {
+      cb(frame);
+
+      if (!frames.has(frame)) {
+        return;
+      }
+
+      Array.forEach(frame.frames, subframe => {
+        if (frames.has(subframe)) {
+          cb(subframe);
+        }
+      });
+    }
+
+    walk(this.content);
+  },
+
+  /**
+   * Stores a given |frame| and its children in the frame tree.
+   *
+   * @param frame (nsIDOMWindow)
+   * @param index (int)
+   *        The index in the given frame's parent's child list.
+   */
+  collect(frame, index = 0) {
+    // Mark the given frame as contained in the frame tree.
+    this._frames.set(frame, index);
+
+    // Mark the given frame's subframes as contained in the tree.
+    Array.forEach(frame.frames, this.collect, this);
+  },
+
+  /**
+   * @see nsIWebProgressListener.onStateChange
+   *
+   * We want to be notified about:
+   *  - new documents that start loading to clear the current frame tree;
+   *  - completed document loads to recollect reachable frames.
+   */
+  onStateChange(webProgress, request, stateFlags, status) {
+    // Ignore state changes for subframes because we're only interested in the
+    // top-document starting or stopping its load. We thus only care about any
+    // changes to the root of the frame tree, not to any of its nodes/leafs.
+    if (!webProgress.isTopLevel || webProgress.DOMWindow != this.content) {
+      return;
+    }
+
+    // onStateChange will be fired when loading the initial about:blank URI for
+    // a browser, which we don't actually care about. This is particularly for
+    // the case of unrestored background tabs, where the content has not yet
+    // been restored: we don't want to accidentally send any updates to the
+    // parent when the about:blank placeholder page has loaded.
+    if (!this._chromeGlobal.docShell.hasLoadedNonBlankURI) {
+      return;
+    }
+
+    if (stateFlags & Ci.nsIWebProgressListener.STATE_START) {
+      // Clear the list of frames until we can recollect it.
+      this._frames = new WeakMap();
+
+      // Notify observers that the frame tree has been reset.
+      this.notifyObservers("onFrameTreeReset");
+    } else if (stateFlags & Ci.nsIWebProgressListener.STATE_STOP) {
+      // The document and its resources have finished loading.
+      this.collect(webProgress.DOMWindow);
+
+      // Notify observers that the frame tree has been reset.
+      this.notifyObservers("onFrameTreeCollected");
+    }
+  },
+
+  QueryInterface: XPCOMUtils.generateQI([Ci.nsIWebProgressListener,
+                                         Ci.nsISupportsWeakReference])
+};
--- a/browser/components/sessionstore/SessionStorage.jsm
+++ b/browser/components/sessionstore/SessionStorage.jsm
@@ -2,28 +2,24 @@
 * 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/. */
 
 "use strict";
 
 this.EXPORTED_SYMBOLS = ["SessionStorage"];
 
 const Cu = Components.utils;
-const Cc = Components.classes;
 const Ci = Components.interfaces;
 
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "console",
   "resource://gre/modules/Console.jsm");
 
-const ssu = Cc["@mozilla.org/browser/sessionstore/utils;1"]
-              .createInstance(Ci.nsISessionStoreUtils);
-
 // A bound to the size of data to store for DOM Storage.
 const DOM_STORAGE_LIMIT_PREF = "browser.sessionstore.dom_storage_limit";
 
 // Returns the principal for a given |frame| contained in a given |docShell|.
 function getPrincipalForFrame(docShell, frame) {
   let ssm = Services.scriptSecurityManager;
   let uri = frame.document.documentURIObject;
   return ssm.getDocShellCodebasePrincipal(uri, docShell);
@@ -53,46 +49,32 @@ this.SessionStorage = Object.freeze({
    *        keys and per-origin session storage data as strings. For example:
    *        {"https://example.com^userContextId=1": {"key": "value", "my_number": "123"}}
    */
   restore(aDocShell, aStorageData) {
     SessionStorageInternal.restore(aDocShell, aStorageData);
   },
 });
 
-/**
- * Calls the given callback |cb|, passing |frame| and each of its descendants.
- */
-function forEachNonDynamicChildFrame(frame, cb) {
-  // Call for current frame.
-  cb(frame);
-
-  // Call the callback recursively for each descendant.
-  ssu.forEachNonDynamicChildFrame(frame, subframe => {
-    return forEachNonDynamicChildFrame(subframe, cb);
-  });
-}
-
 var SessionStorageInternal = {
   /**
    * Reads all session storage data from the given docShell.
-   * @param content
-   *        A tab's global, i.e. the root frame we want to collect for.
+   * @param docShell
+   *        A tab's docshell (containing the sessionStorage)
+   * @param frameTree
+   *        The docShell's FrameTree instance.
    * @return Returns a nested object that will have hosts as keys and per-origin
    *         session storage data as strings. For example:
    *         {"https://example.com^userContextId=1": {"key": "value", "my_number": "123"}}
    */
-  collect(content) {
+  collect(docShell, frameTree) {
     let data = {};
     let visitedOrigins = new Set();
-    let docShell = content.QueryInterface(Ci.nsIInterfaceRequestor)
-                          .getInterface(Ci.nsIWebNavigation)
-                          .QueryInterface(Ci.nsIDocShell);
 
-    forEachNonDynamicChildFrame(content, frame => {
+    frameTree.forEach(frame => {
       let principal = getPrincipalForFrame(docShell, frame);
       if (!principal) {
         return;
       }
 
       // Get the origin of the current history entry
       // and use that as a key for the per-principal storage data.
       let origin;
--- a/browser/components/sessionstore/content/content-sessionStore.js
+++ b/browser/components/sessionstore/content/content-sessionStore.js
@@ -29,23 +29,23 @@ XPCOMUtils.defineLazyModuleGetter(this, 
   "resource:///modules/sessionstore/DocShellCapabilities.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "ScrollPosition",
   "resource://gre/modules/ScrollPosition.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "SessionHistory",
   "resource://gre/modules/sessionstore/SessionHistory.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "SessionStorage",
   "resource:///modules/sessionstore/SessionStorage.jsm");
 
+Cu.import("resource:///modules/sessionstore/FrameTree.jsm", this);
+var gFrameTree = new FrameTree(this);
+
 Cu.import("resource:///modules/sessionstore/ContentRestore.jsm", this);
 XPCOMUtils.defineLazyGetter(this, "gContentRestore",
                             () => { return new ContentRestore(this) });
 
-const ssu = Cc["@mozilla.org/browser/sessionstore/utils;1"]
-              .getService(Ci.nsISessionStoreUtils);
-
 // The current epoch.
 var gCurrentEpoch = 0;
 
 // A bound to the size of data to store for DOM Storage.
 const DOM_STORAGE_LIMIT_PREF = "browser.sessionstore.dom_storage_limit";
 
 // This pref controls whether or not we send updates to the parent on a timeout
 // or not, and should only be used for tests or debugging.
@@ -68,115 +68,23 @@ function createLazy(fn) {
       cached = true;
     }
 
     return cachedValue;
   };
 }
 
 /**
- * A function that will recursively call |cb| to collected data for all
- * non-dynamic frames in the current frame/docShell tree.
- */
-function mapFrameTree(cb) {
-  return (function map(frame, cb) {
-    // Collect data for the current frame.
-    let obj = cb(frame) || {};
-    let children = [];
-
-    // Recurse into child frames.
-    ssu.forEachNonDynamicChildFrame(frame, (subframe, index) => {
-      let result = map(subframe, cb);
-      if (result && Object.keys(result).length) {
-        children[index] = result;
-      }
-    });
-
-    if (children.length) {
-      obj.children = children;
-    }
-
-    return Object.keys(obj).length ? obj : null;
-  })(content, cb);
-}
-
-/**
- * Listens for state change notifcations from webProgress and notifies each
- * registered observer for either the start of a page load, or its completion.
- */
-var StateChangeNotifier = {
-
-  init() {
-    this._observers = new Set();
-    let ifreq = docShell.QueryInterface(Ci.nsIInterfaceRequestor);
-    let webProgress = ifreq.getInterface(Ci.nsIWebProgress);
-    webProgress.addProgressListener(this, Ci.nsIWebProgress.NOTIFY_STATE_DOCUMENT);
-  },
-
-  /**
-   * Adds a given observer |obs| to the set of observers that will be notified
-   * when when a new document starts or finishes loading.
-   *
-   * @param obs (object)
-   */
-  addObserver(obs) {
-    this._observers.add(obs);
-  },
-
-  /**
-   * Notifies all observers that implement the given |method|.
-   *
-   * @param method (string)
-   */
-  notifyObservers(method) {
-    for (let obs of this._observers) {
-      if (obs.hasOwnProperty(method)) {
-        obs[method]();
-      }
-    }
-  },
-
-  /**
-   * @see nsIWebProgressListener.onStateChange
-   */
-  onStateChange(webProgress, request, stateFlags, status) {
-    // Ignore state changes for subframes because we're only interested in the
-    // top-document starting or stopping its load.
-    if (!webProgress.isTopLevel || webProgress.DOMWindow != content) {
-      return;
-    }
-
-    // onStateChange will be fired when loading the initial about:blank URI for
-    // a browser, which we don't actually care about. This is particularly for
-    // the case of unrestored background tabs, where the content has not yet
-    // been restored: we don't want to accidentally send any updates to the
-    // parent when the about:blank placeholder page has loaded.
-    if (!docShell.hasLoadedNonBlankURI) {
-      return;
-    }
-
-    if (stateFlags & Ci.nsIWebProgressListener.STATE_START) {
-      this.notifyObservers("onPageLoadStarted");
-    } else if (stateFlags & Ci.nsIWebProgressListener.STATE_STOP) {
-      this.notifyObservers("onPageLoadCompleted");
-    }
-  },
-
-  QueryInterface: XPCOMUtils.generateQI([Ci.nsIWebProgressListener,
-                                         Ci.nsISupportsWeakReference])
-};
-
-/**
  * Listens for and handles content events that we need for the
  * session store service to be notified of state changes in content.
  */
 var EventListener = {
 
   init() {
-    addEventListener("load", ssu.createDynamicFrameEventFilter(this), true);
+    addEventListener("load", this, true);
   },
 
   handleEvent(event) {
     // Ignore load events from subframes.
     if (event.target != content.document) {
       return;
     }
 
@@ -335,20 +243,20 @@ var MessageListener = {
  * Causes a SessionStore:update message to be sent that contains the current
  * session history.
  *
  * Example:
  *   {entries: [{url: "about:mozilla", ...}, ...], index: 1}
  */
 var SessionHistoryListener = {
   init() {
-    // The state change observer is needed to handle initial subframe loads.
+    // The frame tree observer is needed to handle initial subframe loads.
     // It will redundantly invalidate with the SHistoryListener in some cases
     // but these invalidations are very cheap.
-    StateChangeNotifier.addObserver(this);
+    gFrameTree.addObserver(this);
 
     // By adding the SHistoryListener immediately, we will unfortunately be
     // notified of every history entry as the tab is restored. We don't bother
     // waiting to add the listener later because these notifications are cheap.
     // We will likely only collect once since we are batching collection on
     // a delay.
     docShell.QueryInterface(Ci.nsIWebNavigation).sessionHistory.
       addSHistoryListener(this);
@@ -416,21 +324,21 @@ var SessionHistoryListener = {
       return history;
     });
   },
 
   handleEvent(event) {
     this.collect();
   },
 
-  onPageLoadCompleted() {
+  onFrameTreeCollected() {
     this.collect();
   },
 
-  onPageLoadStarted() {
+  onFrameTreeReset() {
     this.collect();
   },
 
   OnHistoryNewEntry(newURI, oldIndex) {
     // We ought to collect the previously current entry as well, see bug 1350567.
     this.collectFrom(oldIndex);
   },
 
@@ -489,34 +397,40 @@ var SessionHistoryListener = {
  * is scrolled this will return null so that we don't tack a property onto
  * the tabData object in the parent process.
  *
  * Example:
  *   {scroll: "100,100", children: [null, null, {scroll: "200,200"}]}
  */
 var ScrollPositionListener = {
   init() {
-    addEventListener("scroll", ssu.createDynamicFrameEventFilter(this));
-    StateChangeNotifier.addObserver(this);
+    addEventListener("scroll", this);
+    gFrameTree.addObserver(this);
   },
 
-  handleEvent() {
+  handleEvent(event) {
+    let frame = event.target.defaultView;
+
+    // Don't collect scroll data for frames created at or after the load event
+    // as SessionStore can't restore scroll data for those.
+    if (gFrameTree.contains(frame)) {
+      MessageQueue.push("scroll", () => this.collect());
+    }
+  },
+
+  onFrameTreeCollected() {
     MessageQueue.push("scroll", () => this.collect());
   },
 
-  onPageLoadCompleted() {
-    MessageQueue.push("scroll", () => this.collect());
-  },
-
-  onPageLoadStarted() {
+  onFrameTreeReset() {
     MessageQueue.push("scroll", () => null);
   },
 
   collect() {
-    return mapFrameTree(ScrollPosition.collect);
+    return gFrameTree.map(ScrollPosition.collect);
   }
 };
 
 /**
  * Listens for changes to input elements. Whenever the value of an input
  * element changes we will re-collect data for the current frame tree and send
  * a message to the parent process.
  *
@@ -529,30 +443,36 @@ var ScrollPositionListener = {
  *     children: [
  *       null,
  *       {url: "http://sub.mozilla.org/", id: {input_id: "input value 2"}}
  *     ]
  *   }
  */
 var FormDataListener = {
   init() {
-    addEventListener("input", ssu.createDynamicFrameEventFilter(this), true);
-    StateChangeNotifier.addObserver(this);
+    addEventListener("input", this, true);
+    gFrameTree.addObserver(this);
   },
 
-  handleEvent() {
-    MessageQueue.push("formdata", () => this.collect());
+  handleEvent(event) {
+    let frame = event.target.ownerGlobal;
+
+    // Don't collect form data for frames created at or after the load event
+    // as SessionStore can't restore form data for those.
+    if (gFrameTree.contains(frame)) {
+      MessageQueue.push("formdata", () => this.collect());
+    }
   },
 
-  onPageLoadStarted() {
+  onFrameTreeReset() {
     MessageQueue.push("formdata", () => null);
   },
 
   collect() {
-    return mapFrameTree(FormData.collect);
+    return gFrameTree.map(FormData.collect);
   }
 };
 
 /**
  * Listens for changes to docShell capabilities. Whenever a new load is started
  * we need to re-check the list of capabilities and send message when it has
  * changed.
  *
@@ -563,20 +483,23 @@ var FormDataListener = {
 var DocShellCapabilitiesListener = {
   /**
    * This field is used to compare the last docShell capabilities to the ones
    * that have just been collected. If nothing changed we won't send a message.
    */
   _latestCapabilities: "",
 
   init() {
-    StateChangeNotifier.addObserver(this);
+    gFrameTree.addObserver(this);
   },
 
-  onPageLoadStarted() {
+  /**
+   * onFrameTreeReset() is called as soon as we start loading a page.
+   */
+  onFrameTreeReset() {
     // The order of docShell capabilities cannot change while we're running
     // so calling join() without sorting before is totally sufficient.
     let caps = DocShellCapabilities.collect(docShell).join(",");
 
     // Send new data only when the capability list changes.
     if (caps != this._latestCapabilities) {
       this._latestCapabilities = caps;
       MessageQueue.push("disallow", () => caps || null);
@@ -590,26 +513,31 @@ var DocShellCapabilitiesListener = {
  * message to the parent process containing up-to-date sessionStorage data.
  *
  * Causes a SessionStore:update message to be sent that contains the current
  * DOMSessionStorage contents. The data is a nested object using host names
  * as keys and per-host DOMSessionStorage data as values.
  */
 var SessionStorageListener = {
   init() {
-    let filter = ssu.createDynamicFrameEventFilter(this);
-    addEventListener("MozSessionStorageChanged", filter, true);
+    addEventListener("MozSessionStorageChanged", this, true);
     Services.obs.addObserver(this, "browser:purge-domain-data");
-    StateChangeNotifier.addObserver(this);
+    gFrameTree.addObserver(this);
   },
 
   uninit() {
     Services.obs.removeObserver(this, "browser:purge-domain-data");
   },
 
+  handleEvent(event) {
+    if (gFrameTree.contains(event.target)) {
+      this.collectFromEvent(event);
+    }
+  },
+
   observe() {
     // Collect data on the next tick so that any other observer
     // that needs to purge data can do its work first.
     setTimeout(() => this.collect(), 0);
   },
 
   // We don't want to send all the session storage data for all the frames
   // for every change. So if only a few value changed we send them over as
@@ -617,17 +545,17 @@ var SessionStorageListener = {
   // changes we have to send over the entire sessions storage data, we just
   // reset these changes.
   _changes: undefined,
 
   resetChanges() {
     this._changes = undefined;
   },
 
-  handleEvent(event) {
+  collectFromEvent(event) {
     if (!docShell) {
       return;
     }
 
     // How much data does DOMSessionStorage contain?
     let usage = content.QueryInterface(Ci.nsIInterfaceRequestor)
                        .getInterface(Ci.nsIDOMWindowUtils)
                        .getStorageUsage(event.storageArea);
@@ -665,24 +593,26 @@ var SessionStorageListener = {
     if (!docShell) {
       return;
     }
 
     // We need the entire session storage, let's reset the pending individual change
     // messages.
     this.resetChanges();
 
-    MessageQueue.push("storage", () => SessionStorage.collect(content));
+    MessageQueue.push("storage", () => {
+      return SessionStorage.collect(docShell, gFrameTree);
+    });
   },
 
-  onPageLoadCompleted() {
+  onFrameTreeCollected() {
     this.collect();
   },
 
-  onPageLoadStarted() {
+  onFrameTreeReset() {
     this.collect();
   }
 };
 
 /**
  * Listen for changes to the privacy status of the tab.
  * By definition, tabs start in non-private mode.
  *
@@ -860,17 +790,16 @@ var MessageQueue = {
       if (ex && ex.result == Cr.NS_ERROR_OUT_OF_MEMORY) {
         Services.telemetry.getHistogramById("FX_SESSION_RESTORE_SEND_UPDATE_CAUSED_OOM").add(1);
         sendAsyncMessage("SessionStore:error");
       }
     }
   },
 };
 
-StateChangeNotifier.init();
 EventListener.init();
 MessageListener.init();
 FormDataListener.init();
 SessionHistoryListener.init();
 SessionStorageListener.init();
 ScrollPositionListener.init();
 DocShellCapabilitiesListener.init();
 PrivacyListener.init();
@@ -915,12 +844,12 @@ addEventListener("unload", () => {
   // Remove all registered nsIObservers.
   SessionStorageListener.uninit();
   SessionHistoryListener.uninit();
   MessageQueue.uninit();
 
   // Remove progress listeners.
   gContentRestore.resetRestore();
 
-  // We don't need to take care of any StateChangeNotifier observers as they
+  // We don't need to take care of any gFrameTree observers as the gFrameTree
   // will die with the content script. The same goes for the privacy transition
   // observer that will die with the docShell when the tab is closed.
 });
--- a/browser/components/sessionstore/moz.build
+++ b/browser/components/sessionstore/moz.build
@@ -7,30 +7,30 @@
 XPCSHELL_TESTS_MANIFESTS += ['test/unit/xpcshell.ini']
 BROWSER_CHROME_MANIFESTS += ['test/browser.ini']
 
 JAR_MANIFESTS += ['jar.mn']
 
 XPIDL_SOURCES += [
     'nsISessionStartup.idl',
     'nsISessionStore.idl',
-    'nsISessionStoreUtils.idl',
 ]
 
 XPIDL_MODULE = 'sessionstore'
 
 EXTRA_COMPONENTS += [
     'nsSessionStartup.js',
     'nsSessionStore.js',
     'nsSessionStore.manifest',
 ]
 
 EXTRA_JS_MODULES.sessionstore = [
     'ContentRestore.jsm',
     'DocShellCapabilities.jsm',
+    'FrameTree.jsm',
     'GlobalState.jsm',
     'PrivacyFilter.jsm',
     'RecentlyClosedTabsAndWindowsMenuUtils.jsm',
     'RunState.jsm',
     'SessionCookies.jsm',
     'SessionFile.jsm',
     'SessionMigration.jsm',
     'SessionSaver.jsm',
@@ -40,16 +40,10 @@ EXTRA_JS_MODULES.sessionstore = [
     'SessionWorker.jsm',
     'StartupPerformance.jsm',
     'TabAttributes.jsm',
     'TabState.jsm',
     'TabStateCache.jsm',
     'TabStateFlusher.jsm',
 ]
 
-UNIFIED_SOURCES += [
-    'nsSessionStoreUtils.cpp',
-]
-
-FINAL_LIBRARY = 'browsercomps'
-
 with Files('**'):
     BUG_COMPONENT = ('Firefox', 'Session Restore')
deleted file mode 100644
--- a/browser/components/sessionstore/nsISessionStoreUtils.idl
+++ /dev/null
@@ -1,46 +0,0 @@
-/* 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 mozIDOMWindowProxy;
-interface nsIDOMEventListener;
-
-/**
- * A callback passed to nsISessionStoreUtils.forEachNonDynamicChildFrame().
- */
-[function, scriptable, uuid(8199ebf7-76c0-43d6-bcbe-913dd3de3ebf)]
-interface nsISessionStoreUtilsFrameCallback : nsISupports
-{
-  /**
-   * handleFrame() will be called once for each non-dynamic child frame of the
-   * given parent |frame|. The second argument is the |index| of the frame in
-   * the list of all child frames.
-   */
-  void handleFrame(in mozIDOMWindowProxy frame, in unsigned long index);
-};
-
-/**
- * SessionStore utility functions implemented in C++ for performance reasons.
- */
-[scriptable, uuid(2be448ef-c783-45de-a0df-442bccbb4532)]
-interface nsISessionStoreUtils : nsISupports
-{
-  /**
-   * Calls the given |callback| once for each non-dynamic child frame of the
-   * given |window|.
-   */
-  void forEachNonDynamicChildFrame(in mozIDOMWindowProxy window,
-                                   in nsISessionStoreUtilsFrameCallback callback);
-
-  /**
-   * Creates and returns an event listener that filters events from dynamic
-   * docShells. It forwards those from non-dynamic docShells to the given
-   * |listener|.
-   *
-   * This is implemented as a native filter, rather than a JS-based one, for
-   * performance reasons.
-   */
-  nsIDOMEventListener createDynamicFrameEventFilter(in nsIDOMEventListener listener);
-};
deleted file mode 100644
--- a/browser/components/sessionstore/nsSessionStoreUtils.cpp
+++ /dev/null
@@ -1,122 +0,0 @@
-/* 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 "nsSessionStoreUtils.h"
-
-#include "mozilla/dom/Event.h"
-#include "nsPIDOMWindow.h"
-#include "nsIDocShell.h"
-
-using namespace mozilla::dom;
-
-namespace {
-
-class DynamicFrameEventFilter final : public nsIDOMEventListener
-{
-public:
-  NS_DECL_CYCLE_COLLECTING_ISUPPORTS
-  NS_DECL_CYCLE_COLLECTION_CLASS(DynamicFrameEventFilter)
-
-  DynamicFrameEventFilter(nsIDOMEventListener* aListener)
-    : mListener(aListener)
-  { }
-
-  NS_IMETHODIMP HandleEvent(nsIDOMEvent* aEvent) override
-  {
-    if (mListener && TargetInNonDynamicDocShell(aEvent)) {
-      mListener->HandleEvent(aEvent);
-    }
-
-    return NS_OK;
-  }
-
-private:
-  ~DynamicFrameEventFilter() { }
-
-  bool TargetInNonDynamicDocShell(nsIDOMEvent* aEvent)
-  {
-    EventTarget* target = aEvent->InternalDOMEvent()->GetTarget();
-    if (!target) {
-      return false;
-    }
-
-    nsPIDOMWindowOuter* outer = target->GetOwnerGlobalForBindings();
-    if (!outer) {
-      return false;
-    }
-
-    nsIDocShell* docShell = outer->GetDocShell();
-    if (!docShell) {
-      return false;
-    }
-
-    bool isDynamic = false;
-    nsresult rv = docShell->GetCreatedDynamically(&isDynamic);
-    return NS_SUCCEEDED(rv) && !isDynamic;
-  }
-
-  nsCOMPtr<nsIDOMEventListener> mListener;
-};
-
-NS_IMPL_CYCLE_COLLECTION(DynamicFrameEventFilter, mListener)
-
-NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(DynamicFrameEventFilter)
-  NS_INTERFACE_MAP_ENTRY(nsISupports)
-  NS_INTERFACE_MAP_ENTRY(nsIDOMEventListener)
-NS_INTERFACE_MAP_END
-
-NS_IMPL_CYCLE_COLLECTING_ADDREF(DynamicFrameEventFilter)
-NS_IMPL_CYCLE_COLLECTING_RELEASE(DynamicFrameEventFilter)
-
-} // anonymous namespace
-
-NS_IMPL_ISUPPORTS(nsSessionStoreUtils, nsISessionStoreUtils)
-
-NS_IMETHODIMP
-nsSessionStoreUtils::ForEachNonDynamicChildFrame(mozIDOMWindowProxy* aWindow,
-                                                 nsISessionStoreUtilsFrameCallback* aCallback)
-{
-  NS_ENSURE_TRUE(aWindow, NS_ERROR_INVALID_ARG);
-
-  nsCOMPtr<nsPIDOMWindowOuter> outer = nsPIDOMWindowOuter::From(aWindow);
-  NS_ENSURE_TRUE(outer, NS_ERROR_FAILURE);
-
-  nsCOMPtr<nsIDocShell> docShell = outer->GetDocShell();
-  NS_ENSURE_TRUE(docShell, NS_ERROR_FAILURE);
-
-  int32_t length;
-  nsresult rv = docShell->GetChildCount(&length);
-  NS_ENSURE_SUCCESS(rv, rv);
-
-  for (int32_t i = 0, idx = 0; i < length; ++i) {
-    nsCOMPtr<nsIDocShellTreeItem> item;
-    docShell->GetChildAt(i, getter_AddRefs(item));
-    NS_ENSURE_TRUE(item, NS_ERROR_FAILURE);
-
-    nsCOMPtr<nsIDocShell> childDocShell(do_QueryInterface(item));
-    NS_ENSURE_TRUE(childDocShell, NS_ERROR_FAILURE);
-
-    bool isDynamic = false;
-    nsresult rv = childDocShell->GetCreatedDynamically(&isDynamic);
-    if (NS_SUCCEEDED(rv) && isDynamic) {
-      continue;
-    }
-
-    aCallback->HandleFrame(item->GetWindow(), idx++);
-  }
-
-  return NS_OK;
-}
-
-NS_IMETHODIMP
-nsSessionStoreUtils::CreateDynamicFrameEventFilter(nsIDOMEventListener* aListener,
-                                                   nsIDOMEventListener** aResult)
-{
-  NS_ENSURE_TRUE(aListener, NS_ERROR_INVALID_ARG);
-
-  nsCOMPtr<nsIDOMEventListener> filter(new DynamicFrameEventFilter(aListener));
-  filter.forget(aResult);
-
-  return NS_OK;
-}
deleted file mode 100644
--- a/browser/components/sessionstore/nsSessionStoreUtils.h
+++ /dev/null
@@ -1,29 +0,0 @@
-/* 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/. */
-
-#ifndef nsSessionStoreUtils_h
-#define nsSessionStoreUtils_h
-
-#include "nsCycleCollectionParticipant.h"
-#include "nsISessionStoreUtils.h"
-#include "nsIDOMEventListener.h"
-#include "nsCOMPtr.h"
-
-#define NS_SESSIONSTOREUTILS_CID \
-  {0xd713b4be, 0x8285, 0x4cab, {0x9c, 0x0e, 0x0b, 0xbc, 0x38, 0xbf, 0xb9, 0x3c}}
-
-#define NS_SESSIONSTOREUTILS_CONTRACTID \
-  "@mozilla.org/browser/sessionstore/utils;1"
-
-class nsSessionStoreUtils final : public nsISessionStoreUtils
-{
-public:
-  NS_DECL_NSISESSIONSTOREUTILS
-  NS_DECL_ISUPPORTS
-
-private:
-  ~nsSessionStoreUtils() { }
-};
-
-#endif // nsSessionStoreUtils_h
--- a/browser/components/sessionstore/test/browser.ini
+++ b/browser/components/sessionstore/test/browser.ini
@@ -12,17 +12,16 @@ skip-if = os == 'linux' && !e10s
 support-files =
   head.js
   content.js
   content-forms.js
   browser_formdata_sample.html
   browser_formdata_xpath_sample.html
   browser_frametree_sample.html
   browser_frametree_sample_frameset.html
-  browser_frametree_sample_iframes.html
   browser_frame_history_index.html
   browser_frame_history_index2.html
   browser_frame_history_index_blank.html
   browser_frame_history_a.html
   browser_frame_history_b.html
   browser_frame_history_c.html
   browser_frame_history_c1.html
   browser_frame_history_c2.html
--- a/browser/components/sessionstore/test/browser_formdata_sample.html
+++ b/browser/components/sessionstore/test/browser_formdata_sample.html
@@ -1,20 +1,20 @@
 <!DOCTYPE html>
 <html lang="en">
   <head>
     <meta charset="utf-8">
     <title>browser_formdata_sample.html</title>
   </head>
   <body>
     <input id="txt" />
-    <iframe id="iframe"></iframe>
 
     <script type="text/javascript">
       let isOuter = window == window.top;
 
       if (isOuter) {
-        let iframe = document.getElementById("iframe");
+        let iframe = document.createElement("iframe");
         iframe.setAttribute("src", "https://example.com" + location.pathname);
+        document.body.appendChild(iframe);
       }
     </script>
   </body>
 </html>
--- a/browser/components/sessionstore/test/browser_frametree.js
+++ b/browser/components/sessionstore/test/browser_frametree.js
@@ -1,115 +1,131 @@
 /* Any copyright is dedicated to the Public Domain.
  * http://creativecommons.org/publicdomain/zero/1.0/ */
 
 "use strict";
 
 const URL = HTTPROOT + "browser_frametree_sample.html";
 const URL_FRAMESET = HTTPROOT + "browser_frametree_sample_frameset.html";
-const URL_IFRAMES = HTTPROOT + "browser_frametree_sample_iframes.html";
 
 /**
- * Check that we correctly enumerate non-dynamic child frames.
+ * This ensures that loading a page normally, aborting a page load, reloading
+ * a page, navigating using the bfcache, and ignoring frames that were
+ * created dynamically work as expect. We expect the frame tree to be reset
+ * when a page starts loading and we also expect a valid frame tree to exist
+ * when it has stopped loading.
  */
 add_task(async function test_frametree() {
-  // Add an empty tab for a start.
+  const FRAME_TREE_SINGLE = { href: URL };
+  const FRAME_TREE_FRAMESET = {
+    href: URL_FRAMESET,
+    children: [{href: URL}, {href: URL}, {href: URL}]
+  };
+
+  // Create a tab with a single frame.
   let tab = BrowserTestUtils.addTab(gBrowser, URL);
   let browser = tab.linkedBrowser;
-  await promiseBrowserLoaded(browser);
-
-  // The page is a single frame with no children.
-  is(await countNonDynamicFrames(browser), 0, "no child frames");
+  await promiseNewFrameTree(browser);
+  await checkFrameTree(browser, FRAME_TREE_SINGLE,
+    "loading a page resets and creates the frame tree correctly");
 
-  // Navigate to a frameset.
+  // Load the frameset and create two frames dynamically, the first on
+  // DOMContentLoaded and the second on load.
+  await sendMessage(browser, "ss-test:createDynamicFrames", {id: "frames", url: URL});
   browser.loadURI(URL_FRAMESET);
-  await promiseBrowserLoaded(browser);
-
-  // The frameset has two frames.
-  is(await countNonDynamicFrames(browser), 2, "two non-dynamic child frames");
+  await promiseNewFrameTree(browser);
+  await checkFrameTree(browser, FRAME_TREE_FRAMESET,
+    "dynamic frames created on or after the load event are ignored");
 
-  // Go back in history.
-  let pageShowPromise = ContentTask.spawn(browser, null, async () => {
-    return ContentTaskUtils.waitForEvent(this, "pageshow", true);
-  });
+  // Go back to the previous single-frame page. There will be no load event as
+  // the page is still in the bfcache. We thus make sure this type of navigation
+  // resets the frame tree.
   browser.goBack();
-  await pageShowPromise;
-
-  // We're at page one again.
-  is(await countNonDynamicFrames(browser), 0, "no child frames");
+  await promiseNewFrameTree(browser);
+  await checkFrameTree(browser, FRAME_TREE_SINGLE,
+    "loading from bfache resets and creates the frame tree correctly");
 
-  // Append a dynamic frame.
-  await ContentTask.spawn(browser, URL, async ([url]) => {
-    let frame = content.document.createElement("iframe");
-    frame.setAttribute("src", url);
-    content.document.body.appendChild(frame);
-    return ContentTaskUtils.waitForEvent(frame, "load");
-  });
+  // Load the frameset again but abort the load early.
+  // The frame tree should still be reset and created.
+  browser.loadURI(URL_FRAMESET);
+  executeSoon(() => browser.stop());
+  await promiseNewFrameTree(browser);
 
-  // The dynamic frame should be ignored.
-  is(await countNonDynamicFrames(browser), 0, "we still have a single root frame");
+  // Load the frameset and check the tree again.
+  await sendMessage(browser, "ss-test:createDynamicFrames", {id: "frames", url: URL});
+  browser.loadURI(URL_FRAMESET);
+  await promiseNewFrameTree(browser);
+  await checkFrameTree(browser, FRAME_TREE_FRAMESET,
+    "reloading a page resets and creates the frame tree correctly");
 
   // Cleanup.
-  await promiseRemoveTab(tab);
+  gBrowser.removeTab(tab);
 });
 
 /**
- * Check that we correctly enumerate non-dynamic child frames.
+ * This test ensures that we ignore frames that were created dynamically at or
+ * after the load event. SessionStore can't handle these and will not restore
+ * or collect any data for them.
  */
 add_task(async function test_frametree_dynamic() {
+  // The frame tree as expected. The first two frames are static
+  // and the third one was created on DOMContentLoaded.
+  const FRAME_TREE = {
+    href: URL_FRAMESET,
+    children: [{href: URL}, {href: URL}, {href: URL}]
+  };
+  const FRAME_TREE_REMOVED = {
+    href: URL_FRAMESET,
+    children: [{href: URL}, {href: URL}]
+  };
+
   // Add an empty tab for a start.
-  let tab = BrowserTestUtils.addTab(gBrowser, URL_IFRAMES);
+  let tab = BrowserTestUtils.addTab(gBrowser, "about:blank");
   let browser = tab.linkedBrowser;
   await promiseBrowserLoaded(browser);
 
-  // The page has two iframes.
-  is(await countNonDynamicFrames(browser), 2, "two non-dynamic child frames");
-  is(await enumerateIndexes(browser), "0,1", "correct indexes 0 and 1");
+  // Create dynamic frames on "DOMContentLoaded" and on "load".
+  await sendMessage(browser, "ss-test:createDynamicFrames", {id: "frames", url: URL});
+  browser.loadURI(URL_FRAMESET);
+  await promiseNewFrameTree(browser);
 
-  // Insert a dynamic frame.
-  await ContentTask.spawn(browser, URL, async ([url]) => {
-    let frame = content.document.createElement("iframe");
-    frame.setAttribute("src", url);
-    content.document.body.insertBefore(frame, content.document.getElementsByTagName("iframe")[1]);
-    return ContentTaskUtils.waitForEvent(frame, "load");
-  });
+  // Check that the frame tree does not contain the frame created on "load".
+  // The two static frames and the one created on DOMContentLoaded must be in
+  // the tree.
+  await checkFrameTree(browser, FRAME_TREE,
+    "frame tree contains first four frames");
 
-  // The page still has two iframes.
-  is(await countNonDynamicFrames(browser), 2, "two non-dynamic child frames");
-  is(await enumerateIndexes(browser), "0,1", "correct indexes 0 and 1");
+  // Remove the last frame in the frameset.
+  await sendMessage(browser, "ss-test:removeLastFrame", {id: "frames"});
+  // Check that the frame tree didn't change.
+  await checkFrameTree(browser, FRAME_TREE,
+    "frame tree contains first four frames");
 
-  // Append a dynamic frame.
-  await ContentTask.spawn(browser, URL, async ([url]) => {
-    let frame = content.document.createElement("iframe");
-    frame.setAttribute("src", url);
-    content.document.body.appendChild(frame);
-    return ContentTaskUtils.waitForEvent(frame, "load");
-  });
-
-  // The page still has two iframes.
-  is(await countNonDynamicFrames(browser), 2, "two non-dynamic child frames");
-  is(await enumerateIndexes(browser), "0,1", "correct indexes 0 and 1");
+  // Remove the last frame in the frameset.
+  await sendMessage(browser, "ss-test:removeLastFrame", {id: "frames"});
+  // Check that the frame tree excludes the removed frame.
+  await checkFrameTree(browser, FRAME_TREE_REMOVED,
+    "frame tree contains first three frames");
 
   // Cleanup.
-  await promiseRemoveTab(tab);
+  gBrowser.removeTab(tab);
 });
 
-async function countNonDynamicFrames(browser) {
-  return await ContentTask.spawn(browser, null, async () => {
-    const ssu = Cc["@mozilla.org/browser/sessionstore/utils;1"]
-                  .getService(Ci.nsISessionStoreUtils);
-
-    let count = 0;
-    ssu.forEachNonDynamicChildFrame(content, () => count++);
-    return count;
+/**
+ * Checks whether the current frame hierarchy of a given |browser| matches the
+ * |expected| frame hierarchy.
+ */
+function checkFrameTree(browser, expected, msg) {
+  return sendMessage(browser, "ss-test:mapFrameTree").then(tree => {
+    is(JSON.stringify(tree), JSON.stringify(expected), msg);
   });
 }
 
-async function enumerateIndexes(browser) {
-  return await ContentTask.spawn(browser, null, async () => {
-    const ssu = Cc["@mozilla.org/browser/sessionstore/utils;1"]
-                  .getService(Ci.nsISessionStoreUtils);
-
-    let indexes = [];
-    ssu.forEachNonDynamicChildFrame(content, (frame, i) => indexes.push(i));
-    return indexes.join(",");
-  });
+/**
+ * Returns a promise that will be resolved when the given |browser| has loaded
+ * and we received messages saying that its frame tree has been reset and
+ * recollected.
+ */
+function promiseNewFrameTree(browser) {
+  let reset = promiseContentMessage(browser, "ss-test:onFrameTreeCollected");
+  let collect = promiseContentMessage(browser, "ss-test:onFrameTreeCollected");
+  return Promise.all([reset, collect]);
 }
deleted file mode 100644
--- a/browser/components/sessionstore/test/browser_frametree_sample_iframes.html
+++ /dev/null
@@ -1,9 +0,0 @@
-<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Frameset//EN">
-<html lang="en">
-  <head>
-    <meta charset="utf-8">
-    <title>browser_frametree_sample_iframes.html</title>
-  </head>
-  <iframe src="browser_frametree_sample.html"></iframe>
-  <iframe src="browser_frametree_sample.html"></iframe>
-</html>
--- a/browser/components/sessionstore/test/browser_sessionHistory.js
+++ b/browser/components/sessionstore/test/browser_sessionHistory.js
@@ -73,24 +73,19 @@ add_task(async function test_pageshow() 
   let tab = BrowserTestUtils.addTab(gBrowser, URL);
   let browser = tab.linkedBrowser;
   await promiseBrowserLoaded(browser);
 
   // Create a second shistory entry.
   browser.loadURI(URL2);
   await promiseBrowserLoaded(browser);
 
-  // Wait until shistory changes.
-  let pageShowPromise = ContentTask.spawn(browser, null, async () => {
-    return ContentTaskUtils.waitForEvent(this, "pageshow", true);
-  });
-
   // Go back to the previous url which is loaded from the bfcache.
   browser.goBack();
-  await pageShowPromise;
+  await promiseContentMessage(browser, "ss-test:onFrameTreeCollected");
   is(browser.currentURI.spec, URL, "correct url after going back");
 
   // Check that loading from bfcache did invalidate shistory.
   await TabStateFlusher.flush(browser);
   let {index} = JSON.parse(ss.getTabState(tab));
   is(index, 1, "first history entry is selected");
 
   // Cleanup.
--- a/browser/components/sessionstore/test/browser_sessionStorage.html
+++ b/browser/components/sessionstore/test/browser_sessionStorage.html
@@ -1,27 +1,26 @@
 <!DOCTYPE html>
 <html lang="en">
   <head>
     <meta charset="utf-8">
     <title>browser_sessionStorage.html</title>
   </head>
   <body>
-    <iframe id="iframe"></iframe>
-
     <script type="text/javascript">
       let isOuter = window == window.top;
       let args = window.location.search.slice(1).split("&");
       let rand = args[0];
 
       if (isOuter) {
-        let iframe = document.getElementById("iframe");
+        let iframe = document.createElement("iframe");
         let isSecure = args.indexOf("secure") > -1;
         let scheme = isSecure ? "https" : "http";
         iframe.setAttribute("src", scheme + "://example.com" + location.pathname + "?" + rand);
+        document.body.appendChild(iframe);
       }
 
       if (sessionStorage.length === 0) {
         sessionStorage.test = (isOuter ? "outer" : "inner") + "-value-" + rand;
         document.title = sessionStorage.test;
       }
     </script>
   </body>
--- a/browser/components/sessionstore/test/content.js
+++ b/browser/components/sessionstore/test/content.js
@@ -5,21 +5,33 @@
 /* eslint-env mozilla/frame-script */
 
 "use strict";
 
 var Cu = Components.utils;
 var Ci = Components.interfaces;
 
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource:///modules/sessionstore/FrameTree.jsm", this);
+var gFrameTree = new FrameTree(this);
 
 function executeSoon(callback) {
   Services.tm.dispatchToMainThread(callback);
 }
 
+gFrameTree.addObserver({
+  onFrameTreeReset() {
+    sendAsyncMessage("ss-test:onFrameTreeReset");
+  },
+
+  onFrameTreeCollected() {
+    sendAsyncMessage("ss-test:onFrameTreeCollected");
+  }
+});
+
 var historyListener = {
   OnHistoryNewEntry() {
     sendAsyncMessage("ss-test:OnHistoryNewEntry");
   },
 
   OnHistoryGoBack() {
     sendAsyncMessage("ss-test:OnHistoryGoBack");
     return true;
@@ -154,16 +166,58 @@ addMessageListener("ss-test:setScrollPos
   frame.addEventListener("scroll", function onScroll(event) {
     if (frame.document == event.target) {
       frame.removeEventListener("scroll", onScroll);
       sendAsyncMessage("ss-test:setScrollPosition");
     }
   });
 });
 
+addMessageListener("ss-test:createDynamicFrames", function({data}) {
+  function createIFrame(rows) {
+    let frames = content.document.getElementById(data.id);
+    frames.setAttribute("rows", rows);
+
+    let frame = content.document.createElement("frame");
+    frame.setAttribute("src", data.url);
+    frames.appendChild(frame);
+  }
+
+  addEventListener("DOMContentLoaded", function onContentLoaded(event) {
+    if (content.document == event.target) {
+      removeEventListener("DOMContentLoaded", onContentLoaded, true);
+      // DOMContentLoaded is fired right after we finished parsing the document.
+      createIFrame("33%, 33%, 33%");
+    }
+  }, true);
+
+  addEventListener("load", function onLoad(event) {
+    if (content.document == event.target) {
+      removeEventListener("load", onLoad, true);
+
+      // Creating this frame on the same tick as the load event
+      // means that it must not be included in the frame tree.
+      createIFrame("25%, 25%, 25%, 25%");
+    }
+  }, true);
+
+  sendAsyncMessage("ss-test:createDynamicFrames");
+});
+
+addMessageListener("ss-test:removeLastFrame", function({data}) {
+  let frames = content.document.getElementById(data.id);
+  frames.lastElementChild.remove();
+  sendAsyncMessage("ss-test:removeLastFrame");
+});
+
+addMessageListener("ss-test:mapFrameTree", function(msg) {
+  let result = gFrameTree.map(frame => ({href: frame.location.href}));
+  sendAsyncMessage("ss-test:mapFrameTree", result);
+});
+
 addMessageListener("ss-test:click", function({data}) {
   content.document.getElementById(data.id).click();
   sendAsyncMessage("ss-test:click");
 });
 
 addEventListener("load", function(event) {
   let subframe = event.target != content.document;
   sendAsyncMessage("ss-test:loadEvent", {subframe, url: event.target.documentURI});
--- a/dom/base/nsFrameLoader.cpp
+++ b/dom/base/nsFrameLoader.cpp
@@ -2407,20 +2407,16 @@ nsFrameLoader::MaybeCreateDocShell()
   // XXXbz why is this in content code, exactly?  We should handle
   // this some other way.....  Not sure how yet.
   nsCOMPtr<nsIDocShellTreeOwner> parentTreeOwner;
   docShell->GetTreeOwner(getter_AddRefs(parentTreeOwner));
   NS_ENSURE_STATE(parentTreeOwner);
   mIsTopLevelContent =
     AddTreeItemToTreeOwner(mDocShell, parentTreeOwner, parentType, docShell);
 
-  if (mIsTopLevelContent) {
-    mDocShell->SetCreatedDynamically(false);
-  }
-
   // Make sure all shells have links back to the content element
   // in the nearest enclosing chrome shell.
   nsCOMPtr<nsIDOMEventTarget> chromeEventHandler;
 
   if (parentType == nsIDocShellTreeItem::typeChrome) {
     // Our parent shell is a chrome shell. It is therefore our nearest
     // enclosing chrome shell.
 
--- a/toolkit/modules/sessionstore/FormData.jsm
+++ b/toolkit/modules/sessionstore/FormData.jsm
@@ -96,20 +96,16 @@ function shouldIgnoreNode(node) {
  * The public API exported by this module that allows to collect
  * and restore form data for a document and its subframes.
  */
 this.FormData = Object.freeze({
   collect(frame) {
     return FormDataInternal.collect(frame);
   },
 
-  restore(frame, data) {
-    return FormDataInternal.restore(frame, data);
-  },
-
   restoreTree(root, data) {
     FormDataInternal.restoreTree(root, data);
   }
 });
 
 /**
  * This module's internal API.
  */
@@ -285,24 +281,20 @@ var FormDataInternal = {
    * the same format that FormData.collect() returns.
    *
    * @param frame (DOMWindow)
    *        The frame to restore form data to.
    * @param data (object)
    *        An object holding form data.
    */
   restore({document: doc}, data) {
-    if (!data.url) {
-      return;
-    }
-
     // Don't restore any data for the given frame if the URL
     // stored in the form data doesn't match its current URL.
-    if (data.url != getDocumentURI(doc)) {
-      return false;
+    if (!data.url || data.url != getDocumentURI(doc)) {
+      return;
     }
 
     // For about:{sessionrestore,welcomeback} we saved the field as JSON to
     // avoid nested instances causing humongous sessionstore.js files.
     // cf. bug 467409
     if (hasRestorationData(data)) {
       data.id.sessionData = JSON.stringify(data.id.sessionData);
     }
@@ -444,23 +436,26 @@ var FormDataInternal = {
    *          children: [
    *            {formdata: {id: {input2: "value2"}}},
    *            null,
    *            {formdata: {xpath: { ... }}, children: [ ... ]}
    *          ]
    *        }
    */
   restoreTree(root, data) {
-    // Restore data for the given |root| frame and its descendants. If restore()
-    // returns false this indicates the |data.url| doesn't match the loaded
-    // document URI. We then must ignore this branch for security reasons.
-    if (this.restore(root, data) === false) {
+    // Don't restore any data for the root frame and its subframes if there
+    // is a URL stored in the form data and it doesn't match its current URL.
+    if (data.url && data.url != getDocumentURI(root.document)) {
       return;
     }
 
+    if (data.url) {
+      this.restore(root, data);
+    }
+
     if (!data.hasOwnProperty("children")) {
       return;
     }
 
     let frames = root.frames;
     for (let index of Object.keys(data.children)) {
       if (index < frames.length) {
         this.restoreTree(frames[index], data.children[index]);
--- a/toolkit/modules/sessionstore/ScrollPosition.jsm
+++ b/toolkit/modules/sessionstore/ScrollPosition.jsm
@@ -14,20 +14,16 @@ const Ci = Components.interfaces;
  *
  * This is a child process module.
  */
 this.ScrollPosition = Object.freeze({
   collect(frame) {
     return ScrollPositionInternal.collect(frame);
   },
 
-  restore(frame, value) {
-    ScrollPositionInternal.restore(frame, value);
-  },
-
   restoreTree(root, data) {
     ScrollPositionInternal.restoreTree(root, data);
   }
 });
 
 /**
  * This module's internal API.
  */