Bug 1497147 - Rewrite "mapFrameTree" implementation to C++ r=peterv
authorAlphan Chen <alchen@mozilla.com>
Thu, 28 Feb 2019 13:09:54 +0000
changeset 519608 d7d6b249119a5c8a8b179407f801152f09acb0b3
parent 519607 73298bafb7aec4181e2f10516c4d5e21be42e1d4
child 519609 d2dc3e5300c01da642064140435b9881b13deb22
push id10862
push userffxbld-merge
push dateMon, 11 Mar 2019 13:01:11 +0000
treeherdermozilla-beta@a2e7f5c935da [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerspeterv
bugs1497147
milestone67.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1497147 - Rewrite "mapFrameTree" implementation to C++ r=peterv Get rid of mapFrameTree() implementation Differential Revision: https://phabricator.services.mozilla.com/D17149
browser/components/sessionstore/ContentSessionStore.jsm
dom/chrome-webidl/SessionStoreUtils.webidl
mobile/android/chrome/geckoview/GeckoViewContentChild.js
mobile/android/components/SessionStore.js
toolkit/components/sessionstore/SessionStoreUtils.cpp
toolkit/components/sessionstore/SessionStoreUtils.h
toolkit/modules/sessionstore/Utils.jsm
--- a/browser/components/sessionstore/ContentSessionStore.jsm
+++ b/browser/components/sessionstore/ContentSessionStore.jsm
@@ -13,40 +13,29 @@ ChromeUtils.import("resource://gre/modul
 function debug(msg) {
   Services.console.logStringMessage("SessionStoreContent: " + msg);
 }
 
 ChromeUtils.defineModuleGetter(this, "ContentRestore",
   "resource:///modules/sessionstore/ContentRestore.jsm");
 ChromeUtils.defineModuleGetter(this, "SessionHistory",
   "resource://gre/modules/sessionstore/SessionHistory.jsm");
-ChromeUtils.defineModuleGetter(this, "Utils",
-  "resource://gre/modules/sessionstore/Utils.jsm");
 
 // 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.
 const TIMEOUT_DISABLED_PREF = "browser.sessionstore.debug.no_auto_updates";
 
 const PREF_INTERVAL = "browser.sessionstore.interval";
 
 const kNoIndex = Number.MAX_SAFE_INTEGER;
 const kLastIndex = Number.MAX_SAFE_INTEGER - 1;
 
-/**
- * A function that will recursively call |cb| to collect data for all
- * non-dynamic frames in the current frame/docShell tree.
- */
-function mapFrameTree(mm, callback) {
-  let [data] = Utils.mapFrameTree(mm.content, callback);
-  return data;
-}
-
 class Handler {
   constructor(store) {
     this.store = store;
   }
 
   get contentRestore() {
     return this.store.contentRestore;
   }
@@ -336,17 +325,17 @@ class ScrollPositionListener extends Han
     this.messageQueue.push("scroll", () => this.collect());
   }
 
   onPageLoadStarted() {
     this.messageQueue.push("scroll", () => null);
   }
 
   collect() {
-    return mapFrameTree(this.mm, SessionStoreUtils.collectScrollPosition);
+    return SessionStoreUtils.collectScrollPosition(this.mm.content);
   }
 }
 
 /**
  * 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.
  *
@@ -374,17 +363,17 @@ class FormDataListener extends Handler {
     this.messageQueue.push("formdata", () => this.collect());
   }
 
   onPageLoadStarted() {
     this.messageQueue.push("formdata", () => null);
   }
 
   collect() {
-    return mapFrameTree(this.mm, SessionStoreUtils.collectFormData);
+    return SessionStoreUtils.collectFormData(this.mm.content);
   }
 }
 
 /**
  * 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.
  *
--- a/dom/chrome-webidl/SessionStoreUtils.webidl
+++ b/dom/chrome-webidl/SessionStoreUtils.webidl
@@ -72,25 +72,25 @@ namespace SessionStoreUtils {
    * Collects scroll position data for any given |frame| in the frame hierarchy.
    *
    * @param document (DOMDocument)
    *
    * @return {scroll: "x,y"} e.g. {scroll: "100,200"}
    *         Returns null when there is no scroll data we want to store for the
    *         given |frame|.
    */
-  SSScrollPositionDict collectScrollPosition(Document document);
+  CollectedData? collectScrollPosition(WindowProxy window);
 
   /**
    * Restores scroll position data for any given |frame| in the frame hierarchy.
    *
    * @param frame (DOMWindow)
    * @param value (object, see collectScrollPosition())
    */
-  void restoreScrollPosition(Window frame, optional SSScrollPositionDict data);
+  void restoreScrollPosition(Window frame, optional CollectedData data);
 
   /**
    * Collect form data for a given |frame| *not* including any subframes.
    *
    * The returned object may have an "id", "xpath", or "innerHTML" key or a
    * combination of those three. Form data stored under "id" is for input
    * fields with id attributes. Data stored under "xpath" is used for input
    * fields that don't have a unique id and need to be queried using XPath.
@@ -100,23 +100,22 @@ namespace SessionStoreUtils {
    *   {
    *     id: {input1: "value1", input3: "value3"},
    *     xpath: {
    *       "/xhtml:html/xhtml:body/xhtml:input[@name='input2']" : "value2",
    *       "/xhtml:html/xhtml:body/xhtml:input[@name='input4']" : "value4"
    *     }
    *   }
    *
-   * @param  doc
-   *         DOMDocument instance to obtain form data for.
    * @return object
-   *         Form data encoded in an object.
+   *         Returns null when there is no scroll data
    */
-  CollectedFormData collectFormData(Document document);
-  boolean restoreFormData(Document document, optional CollectedFormData data);
+  CollectedData? collectFormData(WindowProxy window);
+
+  boolean restoreFormData(Document document, optional CollectedData data);
 
   /**
    * Updates all sessionStorage "super cookies"
    * @param content
    *        A tab's global, i.e. the root frame we want to collect for.
    * @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"}}
@@ -130,34 +129,33 @@ namespace SessionStoreUtils {
    * @param aStorageData
    *        A nested object with storage data to be restored that has hosts as
    *        keys and per-origin session storage data as strings. For example:
    *        {"https://example.com^userContextId=1": {"key": "value", "my_number": "123"}}
    */
    void restoreSessionStorage(nsIDocShell docShell, record<DOMString, record<DOMString, DOMString>> data);
 };
 
-dictionary SSScrollPositionDict {
-  ByteString scroll;
-};
-
 dictionary CollectedFileListValue
 {
   required DOMString type;
   required sequence<DOMString> fileList;
 };
 
 dictionary CollectedNonMultipleSelectValue
 {
   required long selectedIndex;
   required DOMString value;
 };
 
 // object contains either a CollectedFileListValue or a CollectedNonMultipleSelectValue or Sequence<DOMString>
 typedef (DOMString or boolean or object) CollectedFormDataValue;
 
-dictionary CollectedFormData
+dictionary CollectedData
 {
+  ByteString scroll;
   record<DOMString, CollectedFormDataValue> id;
   record<DOMString, CollectedFormDataValue> xpath;
   DOMString innerHTML;
   ByteString url;
+  // mChildren contains CollectedData instances
+  sequence<object?> children;
 };
--- a/mobile/android/chrome/geckoview/GeckoViewContentChild.js
+++ b/mobile/android/chrome/geckoview/GeckoViewContentChild.js
@@ -89,19 +89,18 @@ class GeckoViewContentChild extends Geck
     removeEventListener("MozDOMFullscreen:Exit", this);
     removeEventListener("MozDOMFullscreen:Exited", this);
     removeEventListener("MozDOMFullscreen:Request", this);
     removeEventListener("contextmenu", this, { capture: true });
   }
 
   collectSessionState() {
     let history = SessionHistory.collect(docShell);
-    let [formdata, scrolldata] = this.Utils.mapFrameTree(
-        content, SessionStoreUtils.collectFormData,
-        SessionStoreUtils.collectScrollPosition);
+    let formdata = SessionStoreUtils.collectFormData(content);
+    let scrolldata = SessionStoreUtils.collectScrollPosition(content);
 
     // Save the current document resolution.
     let zoom = 1;
     let domWindowUtils = content.windowUtils;
     zoom = domWindowUtils.getResolution();
     scrolldata = scrolldata || {};
     scrolldata.zoom = {};
     scrolldata.zoom.resolution = zoom;
--- a/mobile/android/components/SessionStore.js
+++ b/mobile/android/components/SessionStore.js
@@ -872,17 +872,17 @@ SessionStore.prototype = {
     let data = aBrowser.__SS_data;
     if (!data || data.entries.length == 0) {
       sendEvent(aBrowser, "SSTabInputCaptured");
       return;
     }
 
     // Store the form data.
     let content = aBrowser.contentWindow;
-    let [formdata] = Utils.mapFrameTree(content, SessionStoreUtils.collectFormData);
+    let formdata = SessionStoreUtils.collectFormData(content);
     formdata = PrivacyFilter.filterFormData(formdata || {});
 
     // If we found any form data, main content or frames, let's save it
     if (formdata && Object.keys(formdata).length) {
       data.formdata = formdata;
       log("onTabInput() ran for tab " + aWindow.BrowserApp.getTabForBrowser(aBrowser).id);
       this.saveStateDelayed();
     }
@@ -911,18 +911,17 @@ SessionStore.prototype = {
 
     // Neither bother if we're yet to restore the previous scroll position.
     if (aBrowser.__SS_restoreDataOnLoad || aBrowser.__SS_restoreDataOnPageshow) {
       return;
     }
 
     // Save the scroll position itself.
     let content = aBrowser.contentWindow;
-    let [scrolldata] =
-        Utils.mapFrameTree(content, SessionStoreUtils.collectScrollPosition);
+    let scrolldata = SessionStoreUtils.collectScrollPosition(content);
     scrolldata = scrolldata || {};
 
     // Save the current document resolution.
     let zoom = 1;
     zoom = content.windowUtils.getResolution();
     scrolldata.zoom = {};
     scrolldata.zoom.resolution = zoom;
     log("onTabScroll() zoom level: " + zoom);
--- a/toolkit/components/sessionstore/SessionStoreUtils.cpp
+++ b/toolkit/components/sessionstore/SessionStoreUtils.cpp
@@ -247,36 +247,34 @@ SessionStoreUtils::AddDynamicFrameFilter
       aDocShell->SetAllowContentRetargetingOnChildren(
           allow);  // restore the allowProp to original
     } else if (token.EqualsLiteral("ContentRetargetingOnChildren")) {
       aDocShell->SetAllowContentRetargetingOnChildren(false);
     }
   }
 }
 
-/* static */ void SessionStoreUtils::CollectScrollPosition(
-    const GlobalObject& aGlobal, Document& aDocument,
-    SSScrollPositionDict& aRetVal) {
+static void CollectCurrentScrollPosition(JSContext* aCx, Document& aDocument,
+                                         Nullable<CollectedData>& aRetVal) {
   nsIPresShell* presShell = aDocument.GetShell();
   if (!presShell) {
     return;
   }
-
   nsPoint scrollPos = presShell->GetVisualViewportOffset();
   int scrollX = nsPresContext::AppUnitsToIntCSSPixels(scrollPos.x);
   int scrollY = nsPresContext::AppUnitsToIntCSSPixels(scrollPos.y);
 
   if ((scrollX != 0) || (scrollY != 0)) {
-    aRetVal.mScroll.Construct() = nsPrintfCString("%d,%d", scrollX, scrollY);
+    aRetVal.SetValue().mScroll.Construct() = nsPrintfCString("%d,%d", scrollX, scrollY);
   }
 }
 
 /* static */ void SessionStoreUtils::RestoreScrollPosition(
     const GlobalObject& aGlobal, nsGlobalWindowInner& aWindow,
-    const SSScrollPositionDict& aData) {
+    const CollectedData& aData) {
   if (!aData.mScroll.WasPassed()) {
     return;
   }
 
   nsCCharSeparatedTokenizer tokenizer(aData.mScroll.Value(), ',');
   nsAutoCString token(tokenizer.nextToken());
   int pos_X = atoi(token.get());
   token = tokenizer.nextToken();
@@ -321,34 +319,34 @@ static bool IsValidCCNumber(nsAString& a
 
   return numLength >= 12 && total % 10 == 0;
 }
 
 // Limit the number of XPath expressions for performance reasons. See bug
 // 477564.
 static const uint16_t kMaxTraversedXPaths = 100;
 
-// A helper function to append a element into mId or mXpath of CollectedFormData
+// A helper function to append a element into mId or mXpath of CollectedData
 static Record<nsString, OwningStringOrBooleanOrObject>::EntryType*
 AppendEntryToCollectedData(nsINode* aNode, const nsAString& aId,
                            uint16_t& aGeneratedCount,
-                           CollectedFormData& aRetVal) {
+                           Nullable<CollectedData>& aRetVal) {
   Record<nsString, OwningStringOrBooleanOrObject>::EntryType* entry;
   if (!aId.IsEmpty()) {
-    if (!aRetVal.mId.WasPassed()) {
-      aRetVal.mId.Construct();
+    if (!aRetVal.SetValue().mId.WasPassed()) {
+      aRetVal.SetValue().mId.Construct();
     }
-    auto& recordEntries = aRetVal.mId.Value().Entries();
+    auto& recordEntries = aRetVal.SetValue().mId.Value().Entries();
     entry = recordEntries.AppendElement();
     entry->mKey = aId;
   } else {
-    if (!aRetVal.mXpath.WasPassed()) {
-      aRetVal.mXpath.Construct();
+    if (!aRetVal.SetValue().mXpath.WasPassed()) {
+      aRetVal.SetValue().mXpath.Construct();
     }
-    auto& recordEntries = aRetVal.mXpath.Value().Entries();
+    auto& recordEntries = aRetVal.SetValue().mXpath.Value().Entries();
     entry = recordEntries.AppendElement();
     nsAutoString xpath;
     aNode->GenerateXPath(xpath);
     aGeneratedCount++;
     entry->mKey = xpath;
   }
   return entry;
 }
@@ -356,17 +354,17 @@ AppendEntryToCollectedData(nsINode* aNod
 /*
   @param aDocument: DOMDocument instance to obtain form data for.
   @param aGeneratedCount: the current number of XPath expressions in the
                           returned object.
   @return aRetVal: Form data encoded in an object.
  */
 static void CollectFromTextAreaElement(Document& aDocument,
                                        uint16_t& aGeneratedCount,
-                                       CollectedFormData& aRetVal) {
+                                       Nullable<CollectedData>& aRetVal) {
   RefPtr<nsContentList> textlist = NS_GetContentList(
       &aDocument, kNameSpaceID_XHTML, NS_LITERAL_STRING("textarea"));
   uint32_t length = textlist->Length(true);
   for (uint32_t i = 0; i < length; ++i) {
     MOZ_ASSERT(textlist->Item(i), "null item in node list!");
 
     HTMLTextAreaElement* textArea =
         HTMLTextAreaElement::FromNodeOrNull(textlist->Item(i));
@@ -400,17 +398,17 @@ static void CollectFromTextAreaElement(D
 /*
   @param aDocument: DOMDocument instance to obtain form data for.
   @param aGeneratedCount: the current number of XPath expressions in the
                           returned object.
   @return aRetVal: Form data encoded in an object.
  */
 static void CollectFromInputElement(JSContext* aCx, Document& aDocument,
                                     uint16_t& aGeneratedCount,
-                                    CollectedFormData& aRetVal) {
+                                    Nullable<CollectedData>& aRetVal) {
   RefPtr<nsContentList> inputlist = NS_GetContentList(
       &aDocument, kNameSpaceID_XHTML, NS_LITERAL_STRING("input"));
   uint32_t length = inputlist->Length(true);
   for (uint32_t i = 0; i < length; ++i) {
     MOZ_ASSERT(inputlist->Item(i), "null item in node list!");
     nsCOMPtr<nsIFormControl> formControl =
         do_QueryInterface(inputlist->Item(i));
     if (formControl) {
@@ -512,17 +510,17 @@ static void CollectFromInputElement(JSCo
 /*
   @param aDocument: DOMDocument instance to obtain form data for.
   @param aGeneratedCount: the current number of XPath expressions in the
                           returned object.
   @return aRetVal: Form data encoded in an object.
  */
 static void CollectFromSelectElement(JSContext* aCx, Document& aDocument,
                                      uint16_t& aGeneratedCount,
-                                     CollectedFormData& aRetVal) {
+                                     Nullable<CollectedData>& aRetVal) {
   RefPtr<nsContentList> selectlist = NS_GetContentList(
       &aDocument, kNameSpaceID_XHTML, NS_LITERAL_STRING("select"));
   uint32_t length = selectlist->Length(true);
   for (uint32_t i = 0; i < length; ++i) {
     MOZ_ASSERT(selectlist->Item(i), "null item in node list!");
     RefPtr<HTMLSelectElement> select =
         HTMLSelectElement::FromNodeOrNull(selectlist->Item(i));
     if (!select) {
@@ -593,17 +591,17 @@ static void CollectFromSelectElement(JSC
   }
 }
 
 /*
   @param aDocument: DOMDocument instance to obtain form data for.
   @return aRetVal: Form data encoded in an object.
  */
 static void CollectFromXULTextbox(Document& aDocument,
-                                  CollectedFormData& aRetVal) {
+                                  Nullable<CollectedData>& aRetVal) {
   nsAutoCString url;
   Unused << aDocument.GetDocumentURI()->GetSpecIgnoringRef(url);
   if (!url.EqualsLiteral("about:config")) {
     return;
   }
   RefPtr<nsContentList> aboutConfigElements = NS_GetContentList(
       &aDocument, kNameSpaceID_XUL, NS_LITERAL_STRING("window"));
   uint32_t length = aboutConfigElements->Length(true);
@@ -638,44 +636,42 @@ static void CollectFromXULTextbox(Docume
       Record<nsString, OwningStringOrBooleanOrObject>::EntryType* entry =
           AppendEntryToCollectedData(input, id, generatedCount, aRetVal);
       entry->mValue.SetAsString() = value;
       return;
     }
   }
 }
 
-/* static */ void SessionStoreUtils::CollectFormData(
-    const GlobalObject& aGlobal, Document& aDocument,
-    CollectedFormData& aRetVal) {
+static void CollectCurrentFormData(JSContext* aCx, Document& aDocument,
+                                   Nullable<CollectedData>& aRetVal) {
   uint16_t generatedCount = 0;
   /* textarea element */
   CollectFromTextAreaElement(aDocument, generatedCount, aRetVal);
   /* input element */
-  CollectFromInputElement(aGlobal.Context(), aDocument, generatedCount,
-                          aRetVal);
+  CollectFromInputElement(aCx, aDocument, generatedCount, aRetVal);
   /* select element */
-  CollectFromSelectElement(aGlobal.Context(), aDocument, generatedCount,
-                           aRetVal);
+  CollectFromSelectElement(aCx, aDocument, generatedCount, aRetVal);
   /* special case for about:config's search field */
   CollectFromXULTextbox(aDocument, aRetVal);
 
   Element* bodyElement = aDocument.GetBody();
   if (aDocument.HasFlag(NODE_IS_EDITABLE) && bodyElement) {
-    bodyElement->GetInnerHTML(aRetVal.mInnerHTML.Construct(), IgnoreErrors());
+    bodyElement->GetInnerHTML(aRetVal.SetValue().mInnerHTML.Construct(), IgnoreErrors());
   }
-  if (!aRetVal.mId.WasPassed() && !aRetVal.mXpath.WasPassed() &&
-      !aRetVal.mInnerHTML.WasPassed()) {
+
+  if (aRetVal.IsNull()) {
     return;
   }
+
   // Store the frame's current URL with its form data so that we can compare
   // it when restoring data to not inject form data into the wrong document.
   nsIURI* uri = aDocument.GetDocumentURI();
   if (uri) {
-    uri->GetSpecIgnoringRef(aRetVal.mUrl.Construct());
+    uri->GetSpecIgnoringRef(aRetVal.SetValue().mUrl.Construct());
   }
 }
 
 MOZ_CAN_RUN_SCRIPT
 static void SetElementAsString(Element* aElement, const nsAString& aValue) {
   IgnoredErrorResult rv;
   HTMLTextAreaElement* textArea = HTMLTextAreaElement::FromNode(aElement);
   if (textArea) {
@@ -849,17 +845,17 @@ static void SetRestoreData(JSContext* aC
   if (nsContentUtils::StringifyJSON(aCx, aObject, data)) {
     SetElementAsString(aElement, data);
   } else {
     JS_ClearPendingException(aCx);
   }
 }
 
 MOZ_CAN_RUN_SCRIPT
-static void SetInnerHTML(Document& aDocument, const CollectedFormData& aData) {
+static void SetInnerHTML(Document& aDocument, const CollectedData& aData) {
   RefPtr<Element> bodyElement = aDocument.GetBody();
   if (aDocument.HasFlag(NODE_IS_EDITABLE) && bodyElement) {
     IgnoredErrorResult rv;
     bodyElement->SetInnerHTML(aData.mInnerHTML.Value(),
                               aDocument.NodePrincipal(), rv);
     if (!rv.Failed()) {
       nsContentUtils::DispatchInputEvent(bodyElement);
     }
@@ -910,17 +906,17 @@ static Element* FindNodeByXPath(JSContex
     return nullptr;
   }
   return Element::FromNodeOrNull(result->GetSingleNodeValue(rv));
 }
 
 MOZ_CAN_RUN_SCRIPT_BOUNDARY
 /* static */ bool SessionStoreUtils::RestoreFormData(
     const GlobalObject& aGlobal, Document& aDocument,
-    const CollectedFormData& aData) {
+    const CollectedData& aData) {
   if (!aData.mUrl.WasPassed()) {
     return true;
   }
   // Don't restore any data for the given frame if the URL
   // stored in the form data doesn't match its current URL.
   nsAutoCString url;
   Unused << aDocument.GetDocumentURI()->GetSpecIgnoringRef(url);
   if (!aData.mUrl.Value().Equals(url)) {
@@ -1150,8 +1146,78 @@ static void CollectedSessionStorageInter
       IgnoredErrorResult result;
       storage->SetItem(InnerEntry.mKey, InnerEntry.mValue, *principal, result);
       if (result.Failed()) {
         NS_WARNING("storage set item failed!");
       }
     }
   }
 }
+
+typedef void (*CollectorFunc)(JSContext* aCx, Document& aDocument, Nullable<CollectedData>& aRetVal);
+
+/**
+ * A function that will recursively call |CollectorFunc| to collect data for all
+ * non-dynamic frames in the current frame/docShell tree.
+ */
+static void CollectFrameTreeData(JSContext* aCx,
+                                 BrowsingContext* aBrowsingContext,
+                                 Nullable<CollectedData>& aRetVal,
+                                 CollectorFunc aFunc) {
+  nsPIDOMWindowOuter* window = aBrowsingContext->GetDOMWindow();
+  if (!window) {
+    return;
+  }
+
+  nsIDocShell* docShell = window->GetDocShell();
+  if (!docShell || docShell->GetCreatedDynamically()) {
+    return;
+  }
+
+  Document* document = window->GetDoc();
+  if (!document) {
+    return;
+  }
+
+  /* Collect data from current frame */
+  aFunc(aCx, *document, aRetVal);
+
+  /* Collect data from all child frame */
+  nsTArray<JSObject*> childrenData;
+  SequenceRooter<JSObject*> rooter(aCx, &childrenData);
+  uint32_t trailingNullCounter = 0;
+
+  nsTArray<RefPtr<BrowsingContext>> children;
+  aBrowsingContext->GetChildren(children);
+  for (uint32_t i = 0; i < children.Length(); i++) {
+    NullableRootedDictionary<CollectedData> data(aCx);
+    CollectFrameTreeData(aCx, children[i], data, aFunc);
+    if (data.IsNull()) {
+      childrenData.AppendElement(nullptr);
+      trailingNullCounter++;
+      continue;
+    }
+    JS::Rooted<JS::Value> jsval(aCx);
+    if (!ToJSValue(aCx, data.SetValue(), &jsval)) {
+      JS_ClearPendingException(aCx);
+      continue;
+    }
+    childrenData.AppendElement(&jsval.toObject());
+    trailingNullCounter = 0;
+  }
+
+  if (trailingNullCounter != childrenData.Length()) {
+    childrenData.TruncateLength(childrenData.Length() - trailingNullCounter);
+    aRetVal.SetValue().mChildren.Construct().SwapElements(childrenData);
+  }
+}
+
+/* static */ void SessionStoreUtils::CollectScrollPosition(
+    const GlobalObject& aGlobal, WindowProxyHolder& aWindow,
+    Nullable<CollectedData>& aRetVal) {
+  CollectFrameTreeData(aGlobal.Context(), aWindow.get(), aRetVal, CollectCurrentScrollPosition);
+}
+
+/* static */ void SessionStoreUtils::CollectFormData(
+    const GlobalObject& aGlobal, WindowProxyHolder& aWindow,
+    Nullable<CollectedData>& aRetVal) {
+  CollectFrameTreeData(aGlobal.Context(), aWindow.get(), aRetVal, CollectCurrentFormData);
+}
--- a/toolkit/components/sessionstore/SessionStoreUtils.h
+++ b/toolkit/components/sessionstore/SessionStoreUtils.h
@@ -40,29 +40,30 @@ class SessionStoreUtils {
                                           nsIDocShell* aDocShell,
                                           nsCString& aRetVal);
 
   static void RestoreDocShellCapabilities(
       const GlobalObject& aGlobal, nsIDocShell* aDocShell,
       const nsCString& aDisallowCapabilities);
 
   static void CollectScrollPosition(const GlobalObject& aGlobal,
-                                    Document& aDocument,
-                                    SSScrollPositionDict& aRetVal);
+                                    WindowProxyHolder& aWindow,
+                                    Nullable<CollectedData>& aRetVal);
 
   static void RestoreScrollPosition(const GlobalObject& aGlobal,
                                     nsGlobalWindowInner& aWindow,
-                                    const SSScrollPositionDict& data);
+                                    const CollectedData& data);
 
-  static void CollectFormData(const GlobalObject& aGlobal, Document& aDocument,
-                              CollectedFormData& aRetVal);
+  static void CollectFormData(const GlobalObject& aGlobal,
+                              WindowProxyHolder& aWindow,
+                              Nullable<CollectedData>& aRetVal);
 
   MOZ_CAN_RUN_SCRIPT_BOUNDARY
   static bool RestoreFormData(const GlobalObject& aGlobal, Document& aDocument,
-                              const CollectedFormData& aData);
+                              const CollectedData& aData);
 
   static void CollectSessionStorage(
       const GlobalObject& aGlobal, WindowProxyHolder& aWindow,
       Record<nsString, Record<nsString, nsString>>& aRetVal);
 
   static void RestoreSessionStorage(
       const GlobalObject& aGlobal, nsIDocShell* aDocShell,
       const Record<nsString, Record<nsString, nsString>>& aData);
--- a/toolkit/modules/sessionstore/Utils.jsm
+++ b/toolkit/modules/sessionstore/Utils.jsm
@@ -59,62 +59,16 @@ var Utils = Object.freeze({
     for (let key of Object.keys(obj)) {
       retval[key] = obj[key];
     }
 
     return retval;
   },
 
   /**
-   * A function that will recursively call |cb| to collect data for all
-   * non-dynamic frames in the current frame/docShell tree.
-   *
-   * @param {mozIDOMWindowProxy} frame A DOM window or content frame for which
-   *                                   data will be collected.
-   * @param {...function} dataCollectors One or more data collection functions
-   *                                     that will be called once for each non-
-   *                                     dynamic frame in the given frame tree,
-   *                                     and which should return the data they
-   *                                     wish to save for that respective frame.
-   * @return {object[]} An array with one entry per dataCollector, containing
-   *                    the collected data as a nested data structure according
-   *                    to the layout of the frame tree, or null if no data was
-   *                    returned by the respective dataCollector.
-   */
-  mapFrameTree(frame, ...dataCollectors) {
-    // Collect data for the current frame.
-    let objs = dataCollectors.map(dataCollector => dataCollector(frame.document) || {});
-    let children = dataCollectors.map(() => []);
-
-    // Recurse into child frames.
-    SessionStoreUtils.forEachNonDynamicChildFrame(frame, (subframe, index) => {
-      let results = this.mapFrameTree(subframe, ...dataCollectors);
-      if (!results) {
-        return;
-      }
-
-      for (let j = results.length - 1; j >= 0; --j) {
-        if (!results[j] || !Object.getOwnPropertyNames(results[j]).length) {
-          continue;
-        }
-        children[j][index] = results[j];
-      }
-    });
-
-    for (let i = objs.length - 1; i >= 0; --i) {
-      if (!children[i].length) {
-        continue;
-      }
-      objs[i].children = children[i];
-    }
-
-    return objs.map((obj) => Object.getOwnPropertyNames(obj).length ? obj : null);
-  },
-
-  /**
    * 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.
    */
   restoreFrameTreeData(frame, data, cb) {
     // Restore data for the root frame.
     // The callback can abort by returning false.
     if (cb(frame, data) === false) {