Bug 1294502 - Combine e10s and non-e10s nsFormAutoComplete implementations. r=MattN
authorMike Conley <mconley@mozilla.com>
Thu, 11 Aug 2016 15:36:22 -0400
changeset 309941 eaed9150655ae93349c937ff38cc321edbff0e07
parent 309940 fbc71dcb173ac4f83d24653e692b2b2599205d48
child 309942 3ab902adf2ce16e9a385ae2b9cb914b9b314f945
push id31498
push usermconley@mozilla.com
push dateThu, 18 Aug 2016 15:57:43 +0000
treeherderautoland@f2ea401ab10c [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersMattN
bugs1294502
milestone51.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 1294502 - Combine e10s and non-e10s nsFormAutoComplete implementations. r=MattN MozReview-Commit-ID: 46jM5Q3AfRa
toolkit/components/satchel/FormHistoryStartup.js
toolkit/components/satchel/nsFormAutoComplete.js
--- a/toolkit/components/satchel/FormHistoryStartup.js
+++ b/toolkit/components/satchel/FormHistoryStartup.js
@@ -40,16 +40,17 @@ FormHistoryStartup.prototype = {
       case "profile-after-change":
         this.init();
       default:
         break;
     }
   },
 
   inited: false,
+  pendingQuery: null,
 
   init: function()
   {
     if (this.inited)
       return;
     this.inited = true;
 
     Services.prefs.addObserver("browser.formfill.", this, true);
@@ -57,17 +58,24 @@ FormHistoryStartup.prototype = {
     // triggers needed service cleanup and db shutdown
     Services.obs.addObserver(this, "profile-before-change", true);
     Services.obs.addObserver(this, "formhistory-expire-now", true);
 
     let messageManager = Cc["@mozilla.org/globalmessagemanager;1"].
                          getService(Ci.nsIMessageListenerManager);
     messageManager.loadFrameScript("chrome://satchel/content/formSubmitListener.js", true);
     messageManager.addMessageListener("FormHistory:FormSubmitEntries", this);
-    messageManager.addMessageListener("FormHistory:AutoCompleteSearchAsync", this);
+
+    // For each of these messages, we could receive them from content,
+    // or we might receive them from the ppmm if the searchbar is
+    // having its history queried.
+    for (let manager of [messageManager, Services.ppmm]) {
+      manager.addMessageListener("FormHistory:AutoCompleteSearchAsync", this);
+      manager.addMessageListener("FormHistory:RemoveEntry", this);
+    }
   },
 
   receiveMessage: function(message) {
     switch (message.name) {
       case "FormHistory:FormSubmitEntries": {
         let entries = message.data;
         let changes = entries.map(function(entry) {
           return {
@@ -77,16 +85,65 @@ FormHistoryStartup.prototype = {
           }
         });
 
         FormHistory.update(changes);
         break;
       }
 
       case "FormHistory:AutoCompleteSearchAsync": {
-        AutoCompleteE10S.search(message);
+        let { id, searchString, params } = message.data;
+
+        if (this.pendingQuery) {
+          this.pendingQuery.cancel();
+          this.pendingQuery = null;
+        }
+
+        let mm;
+        if (message.target instanceof Ci.nsIMessageListenerManager) {
+          // The target is the PPMM, meaning that the parent process
+          // is requesting FormHistory data on the searchbar.
+          mm = message.target;
+        } else {
+          // Otherwise, the target is a <xul:browser>.
+          mm = message.target.messageManager;
+        }
+
+        let results = [];
+        let processResults = {
+          handleResult: aResult => {
+            results.push(aResult);
+          },
+          handleCompletion: aReason => {
+            // Check that the current query is still the one we created. Our
+            // query might have been canceled shortly before completing, in
+            // that case we don't want to call the callback anymore.
+            if (query == this.pendingQuery) {
+              this.pendingQuery = null;
+              if (!aReason) {
+                mm.sendAsyncMessage("FormHistory:AutoCompleteSearchResults",
+                                    { id, results });
+              }
+            }
+          }
+        };
+
+        let query = FormHistory.getAutoCompleteResults(searchString, params,
+                                                       processResults);
+        this.pendingQuery = query;
         break;
       }
+
+      case "FormHistory:RemoveEntry": {
+        let { inputName, value } = message.data;
+        FormHistory.update({
+          op: "remove",
+          fieldname: inputName,
+          value,
+        });
+        break;
+      }
+
     }
   }
 };
 
 this.NSGetFactory = XPCOMUtils.generateNSGetFactory([FormHistoryStartup]);
--- a/toolkit/components/satchel/nsFormAutoComplete.js
+++ b/toolkit/components/satchel/nsFormAutoComplete.js
@@ -9,27 +9,160 @@ const { classes: Cc, interfaces: Ci, res
 
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.import("resource://gre/modules/Services.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "BrowserUtils",
                                   "resource://gre/modules/BrowserUtils.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "Deprecated",
                                   "resource://gre/modules/Deprecated.jsm");
-XPCOMUtils.defineLazyModuleGetter(this, "FormHistory",
-                                  "resource://gre/modules/FormHistory.jsm");
 
 function isAutocompleteDisabled(aField) {
     if (aField.autocomplete !== "") {
         return aField.autocomplete === "off";
     }
 
     return aField.form && aField.form.autocomplete === "off";
 }
 
+/**
+ * An abstraction to talk with the FormHistory database over
+ * the message layer. FormHistoryClient will take care of
+ * figuring out the most appropriate message manager to use,
+ * and what things to send.
+ *
+ * It is assumed that nsFormAutoComplete will only ever use
+ * one instance at a time, and will not attempt to perform more
+ * than one search request with the same instance at a time.
+ * However, nsFormAutoComplete might call remove() any number of
+ * times with the same instance of the client.
+ *
+ * @param Object with the following properties:
+ *
+ *        formField (DOM node):
+ *          A DOM node that we're requesting form history for.
+ *
+ *        inputName (string):
+ *          The name of the input to do the FormHistory look-up
+ *          with. If this is searchbar-history, then formField
+ *          needs to be null, otherwise constructing will throw.
+ */
+function FormHistoryClient({ formField, inputName }) {
+    if (formField && inputName != this.SEARCHBAR_ID) {
+        let window = formField.ownerDocument.defaultView;
+        let topDocShell = window.QueryInterface(Ci.nsIInterfaceRequestor)
+                             .getInterface(Ci.nsIDocShell)
+                             .sameTypeRootTreeItem
+                             .QueryInterface(Ci.nsIDocShell);
+        this.mm = topDocShell.QueryInterface(Ci.nsIInterfaceRequestor)
+                             .getInterface(Ci.nsIContentFrameMessageManager);
+    } else {
+        if (inputName == this.SEARCHBAR_ID) {
+          if (formField) {
+              throw new Error("FormHistoryClient constructed with both a " +
+                              "formField and an inputName. This is not " +
+                              "supported, and only empty results will be " +
+                              "returned.");
+          }
+        }
+        this.mm = Services.cpmm;
+    }
+
+    this.inputName = inputName;
+    this.id = FormHistoryClient.nextRequestID++;
+}
+
+FormHistoryClient.prototype = {
+    SEARCHBAR_ID: "searchbar-history",
+
+    // It is assumed that nsFormAutoComplete only uses / cares about
+    // one FormHistoryClient at a time, and won't attempt to have
+    // multiple in-flight searches occurring with the same FormHistoryClient.
+    // We use an ID number per instantiated FormHistoryClient to make
+    // sure we only respond to messages that were meant for us.
+    id: 0,
+    callback: null,
+    inputName: "",
+    mm: null,
+
+    /**
+     * Query FormHistory for some results.
+     *
+     * @param searchString (string)
+     *        The string to search FormHistory for. See
+     *        FormHistory.getAutoCompleteResults.
+     * @param params (object)
+     *        An Object with search properties. See
+     *        FormHistory.getAutoCompleteResults.
+     * @param callback
+     *        A callback function that will take a single
+     *        argument (the found entries).
+     */
+    requestAutoCompleteResults(searchString, params, callback) {
+        this.mm.sendAsyncMessage("FormHistory:AutoCompleteSearchAsync", {
+            id: this.id,
+            searchString,
+            params,
+        });
+
+        this.mm.addMessageListener("FormHistory:AutoCompleteSearchResults",
+                                   this);
+        this.callback = callback;
+    },
+
+    /**
+     * Cancel an in-flight results request. This ensures that the
+     * callback that requestAutoCompleteResults was passed is never
+     * called from this FormHistoryClient.
+     */
+    cancel() {
+        this.clearListeners();
+    },
+
+    /**
+     * Remove an item from FormHistory.
+     *
+     * @param value (string)
+     *
+     *        The value to remove for this particular
+     *        field.
+     */
+    remove(value) {
+        this.mm.sendAsyncMessage("FormHistory:RemoveEntry", {
+            inputName: this.inputName,
+            value,
+        });
+    },
+
+    // Private methods
+
+    receiveMessage(msg) {
+        let { id, results } = msg.data;
+        if (id != this.id) {
+            return;
+        }
+        if (!this.callback) {
+            Cu.reportError("FormHistoryClient received message with no " +
+                           "callback");
+            return;
+        }
+        this.callback(results);
+        this.clearListeners();
+    },
+
+    clearListeners() {
+        this.mm.removeMessageListener("FormHistory:AutoCompleteSearchResults",
+                                      this);
+        this.callback = null;
+    },
+};
+
+FormHistoryClient.nextRequestID = 1;
+
+
 function FormAutoComplete() {
     this.init();
 }
 
 /**
  * FormAutoComplete
  *
  * Implements the nsIFormAutoComplete interface in the main process.
@@ -44,22 +177,22 @@ FormAutoComplete.prototype = {
     _agedWeight         : 2,
     _bucketSize         : 1,
     _maxTimeGroupings   : 25,
     _timeGroupingSize   : 7 * 24 * 60 * 60 * 1000 * 1000,
     _expireDays         : null,
     _boundaryWeight     : 25,
     _prefixWeight       : 5,
 
-    // Only one query is performed at a time, which will be stored in _pendingQuery
-    // while the query is being performed. It will be cleared when the query finishes,
-    // is cancelled, or an error occurs. If a new query occurs while one is already
-    // pending, the existing one is cancelled. The pending query will be an
-    // mozIStoragePendingStatement object.
-    _pendingQuery       : null,
+    // Only one query via FormHistoryClient is performed at a time, and the
+    // most recent FormHistoryClient which will be stored in _pendingClient
+    // while the query is being performed. It will be cleared when the query
+    // finishes, is cancelled, or an error occurs. If a new query occurs while
+    // one is already pending, the existing one is cancelled.
+    _pendingClient       : null,
 
     init : function() {
         // Preferences. Add observer so we get notified of changes.
         this._prefBranch = Services.prefs.getBranch("browser.formfill.");
         this._prefBranch.addObserver("", this.observer, true);
         this.observer._self = this;
 
         this._debug            = this._prefBranch.getBoolPref("debug");
@@ -158,19 +291,21 @@ FormAutoComplete.prototype = {
         // Guard against void DOM strings filtering into this code.
         if (typeof aInputName === "object") {
             aInputName = "";
         }
         if (typeof aUntrimmedSearchString === "object") {
             aUntrimmedSearchString = "";
         }
 
+        let client = new FormHistoryClient({ formField: aField, inputName: aInputName });
+
         // If we have datalist results, they become our "empty" result.
         let emptyResult = aDatalistResult ||
-                          new FormAutoCompleteResult(FormHistory, [],
+                          new FormAutoCompleteResult(client, [],
                                                      aInputName,
                                                      aUntrimmedSearchString,
                                                      null);
         if (!this._enabled) {
             if (aListener) {
                 aListener.onSearchCompletion(emptyResult);
             }
             return;
@@ -275,17 +410,17 @@ FormAutoComplete.prototype = {
             if (aListener) {
                 aListener.onSearchCompletion(result);
             }
         } else {
             this.log("Creating new autocomplete search result.");
 
             // Start with an empty list.
             let result = aDatalistResult ?
-                new FormAutoCompleteResult(FormHistory, [], aInputName, aUntrimmedSearchString, null) :
+                new FormAutoCompleteResult(client, [], aInputName, aUntrimmedSearchString, null) :
                 emptyResult;
 
             let processEntry = (aEntries) => {
                 if (aField && aField.maxLength > -1) {
                     result.entries =
                         aEntries.filter(function (el) { return el.text.length <= aField.maxLength; });
                 } else {
                     result.entries = aEntries;
@@ -295,17 +430,17 @@ FormAutoComplete.prototype = {
                     result = this.mergeResults(result, aDatalistResult);
                 }
 
                 if (aListener) {
                     aListener.onSearchCompletion(result);
                 }
             }
 
-            this.getAutoCompleteValues(aInputName, searchString, processEntry);
+            this.getAutoCompleteValues(client, aInputName, searchString, processEntry);
         }
     },
 
     mergeResults(historyResult, datalistResult) {
         let values = datalistResult.wrappedJSObject._values;
         let labels = datalistResult.wrappedJSObject._labels;
         let comments = new Array(values.length).fill("");
 
@@ -335,68 +470,50 @@ FormAutoComplete.prototype = {
         let {FormAutoCompleteResult} = Cu.import("resource://gre/modules/nsFormAutoCompleteResult.jsm", {});
         return new FormAutoCompleteResult(datalistResult.searchString,
                                           Ci.nsIAutoCompleteResult.RESULT_SUCCESS,
                                           0, "", finalValues, finalLabels,
                                           finalComments, historyResult);
     },
 
     stopAutoCompleteSearch : function () {
-        if (this._pendingQuery) {
-            this._pendingQuery.cancel();
-            this._pendingQuery = null;
+        if (this._pendingClient) {
+            this._pendingClient.cancel();
+            this._pendingClient = null;
         }
     },
 
     /*
      * Get the values for an autocomplete list given a search string.
      *
+     *  client - a FormHistoryClient instance to perform the search with
      *  fieldName - fieldname field within form history (the form input name)
      *  searchString - string to search for
      *  callback - called when the values are available. Passed an array of objects,
      *             containing properties for each result. The callback is only called
      *             when successful.
      */
-    getAutoCompleteValues : function (fieldName, searchString, callback) {
+    getAutoCompleteValues : function (client, fieldName, searchString, callback) {
         let params = {
             agedWeight:         this._agedWeight,
             bucketSize:         this._bucketSize,
             expiryDate:         1000 * (Date.now() - this._expireDays * 24 * 60 * 60 * 1000),
             fieldname:          fieldName,
             maxTimeGroupings:   this._maxTimeGroupings,
             timeGroupingSize:   this._timeGroupingSize,
             prefixWeight:       this._prefixWeight,
             boundaryWeight:     this._boundaryWeight
         }
 
         this.stopAutoCompleteSearch();
-
-        let results = [];
-        let processResults = {
-          handleResult: aResult => {
-            results.push(aResult);
-          },
-          handleError: aError => {
-            this.log("getAutocompleteValues failed: " + aError.message);
-          },
-          handleCompletion: aReason => {
-            // Check that the current query is still the one we created. Our
-            // query might have been canceled shortly before completing, in
-            // that case we don't want to call the callback anymore.
-            if (query == this._pendingQuery) {
-              this._pendingQuery = null;
-              if (!aReason) {
-                callback(results);
-              }
-            }
-          }
-        };
-
-        let query = FormHistory.getAutoCompleteResults(searchString, params, processResults);
-        this._pendingQuery = query;
+        client.requestAutoCompleteResults(searchString, params, (entries) => {
+            this._pendingClient = null;
+            callback(entries);
+        });
+        this._pendingClient = client;
     },
 
     /*
      * _calculateScore
      *
      * entry    -- an nsIAutoCompleteResult entry
      * aSearchString -- current value of the input (lowercase)
      * searchTokens -- array of tokens of the search string
@@ -416,168 +533,35 @@ FormAutoComplete.prototype = {
         boundaryCalc += this._prefixWeight *
                         (entry.textLowerCase.
                          indexOf(aSearchString) == 0);
         entry.totalScore = Math.round(entry.frecency * Math.max(1, boundaryCalc));
     }
 
 }; // end of FormAutoComplete implementation
 
-/**
- * FormAutoCompleteChild
- *
- * Implements the nsIFormAutoComplete interface in a child content process,
- * and forwards the auto-complete requests to the parent process which
- * also implements a nsIFormAutoComplete interface and has
- * direct access to the FormHistory database.
- */
-function FormAutoCompleteChild() {
-  this.init();
-}
-
-FormAutoCompleteChild.prototype = {
-    classID          : Components.ID("{c11c21b2-71c9-4f87-a0f8-5e13f50495fd}"),
-    QueryInterface   : XPCOMUtils.generateQI([Ci.nsIFormAutoComplete, Ci.nsISupportsWeakReference]),
-
-    _debug: false,
-    _enabled: true,
-    _pendingSearch: null,
-
-    /*
-     * init
-     *
-     * Initializes the content-process side of the FormAutoComplete component,
-     * and add a listener for the message that the parent process sends when
-     * a result is produced.
-     */
-    init: function() {
-      this._debug    = Services.prefs.getBoolPref("browser.formfill.debug");
-      this._enabled  = Services.prefs.getBoolPref("browser.formfill.enable");
-      this.log("init");
-    },
-
-    /*
-     * log
-     *
-     * Internal function for logging debug messages
-     */
-    log : function (message) {
-      if (!this._debug)
-        return;
-      dump("FormAutoCompleteChild: " + message + "\n");
-    },
-
-    autoCompleteSearchAsync : function (aInputName, aUntrimmedSearchString,
-                                        aField, aPreviousResult, aDatalistResult,
-                                        aListener) {
-      this.log("autoCompleteSearchAsync");
-
-      if (this._pendingSearch) {
-        this.stopAutoCompleteSearch();
-      }
-
-      let window = aField.ownerDocument.defaultView;
-
-      let rect = BrowserUtils.getElementBoundingScreenRect(aField);
-      let direction = window.getComputedStyle(aField).direction;
-      let mockField = {};
-      if (isAutocompleteDisabled(aField))
-          mockField.autocomplete = "off";
-      if (aField.maxLength > -1)
-          mockField.maxLength = aField.maxLength;
-
-      let datalistResult = aDatalistResult ?
-        { values: aDatalistResult.wrappedJSObject._values,
-          labels: aDatalistResult.wrappedJSObject._labels} :
-        null;
-
-      let topLevelDocshell = window.QueryInterface(Ci.nsIInterfaceRequestor)
-                                   .getInterface(Ci.nsIDocShell)
-                                   .sameTypeRootTreeItem
-                                   .QueryInterface(Ci.nsIDocShell);
-
-      let mm = topLevelDocshell.QueryInterface(Ci.nsIInterfaceRequestor)
-                               .getInterface(Ci.nsIContentFrameMessageManager);
-
-      mm.sendAsyncMessage("FormHistory:AutoCompleteSearchAsync", {
-        inputName: aInputName,
-        untrimmedSearchString: aUntrimmedSearchString,
-        mockField: mockField,
-        datalistResult: datalistResult,
-        previousSearchString: aPreviousResult && aPreviousResult.searchString.trim().toLowerCase(),
-        left: rect.left,
-        top: rect.top,
-        width: rect.width,
-        height: rect.height,
-        direction: direction,
-      });
-
-      let search = this._pendingSearch = {};
-      let searchFinished = message => {
-        mm.removeMessageListener("FormAutoComplete:AutoCompleteSearchAsyncResult", searchFinished);
-
-        // Check whether stopAutoCompleteSearch() was called, i.e. the search
-        // was cancelled, while waiting for a result.
-        if (search != this._pendingSearch) {
-          return;
-        }
-        this._pendingSearch = null;
-
-        let result = new FormAutoCompleteResult(
-          null,
-          Array.from(message.data.results, res => ({ text: res })),
-          null,
-          aUntrimmedSearchString,
-          mm
-        );
-        if (aListener) {
-          aListener.onSearchCompletion(result);
-        }
-      }
-
-      mm.addMessageListener("FormAutoComplete:AutoCompleteSearchAsyncResult", searchFinished);
-      this.log("autoCompleteSearchAsync message was sent");
-    },
-
-    stopAutoCompleteSearch : function () {
-      this.log("stopAutoCompleteSearch");
-      this._pendingSearch = null;
-    },
-
-    stopControllingInput(aField) {
-      let window = aField.ownerDocument.defaultView;
-      let topLevelDocshell = window.QueryInterface(Ci.nsIInterfaceRequestor)
-                                   .getInterface(Ci.nsIDocShell)
-                                   .sameTypeRootTreeItem
-                                   .QueryInterface(Ci.nsIDocShell);
-      let mm = topLevelDocshell.QueryInterface(Ci.nsIInterfaceRequestor)
-                               .getInterface(Ci.nsIContentFrameMessageManager);
-      mm.sendAsyncMessage("FormAutoComplete:Disconnect");
-    }
-}; // end of FormAutoCompleteChild implementation
-
 // nsIAutoCompleteResult implementation
-function FormAutoCompleteResult(formHistory,
+function FormAutoCompleteResult(client,
                                 entries,
                                 fieldName,
                                 searchString,
                                 messageManager) {
-    this.formHistory = formHistory;
+    this.client = client;
     this.entries = entries;
     this.fieldName = fieldName;
     this.searchString = searchString;
     this.messageManager = messageManager;
 }
 
 FormAutoCompleteResult.prototype = {
     QueryInterface : XPCOMUtils.generateQI([Ci.nsIAutoCompleteResult,
                                             Ci.nsISupportsWeakReference]),
 
     // private
-    formHistory : null,
+    client : null,
     entries : null,
     fieldName : null,
 
     _checkIndexBounds : function (index) {
         if (index < 0 || index >= this.entries.length)
             throw Components.Exception("Index out of range.", Cr.NS_ERROR_ILLEGAL_VALUE);
     },
 
@@ -633,31 +617,14 @@ FormAutoCompleteResult.prototype = {
     },
 
     removeValueAt : function (index, removeFromDB) {
         this._checkIndexBounds(index);
 
         let [removedEntry] = this.entries.splice(index, 1);
 
         if (removeFromDB) {
-            if (this.formHistory) {
-                this.formHistory.update({ op: "remove",
-                                          fieldname: this.fieldName,
-                                          value: removedEntry.text });
-            } else {
-                this.messageManager.sendAsyncMessage("FormAutoComplete:RemoveEntry",
-                                                     { index });
-            }
+            this.client.remove(removedEntry.text);
         }
     }
 };
 
-
-if (Services.appinfo.processType == Services.appinfo.PROCESS_TYPE_CONTENT &&
-    Services.prefs.getBoolPref("browser.tabs.remote.desktopbehavior", false)) {
-  // Register the stub FormAutoComplete module in the child which will
-  // forward messages to the parent through the process message manager.
-  let component = [FormAutoCompleteChild];
-  this.NSGetFactory = XPCOMUtils.generateNSGetFactory(component);
-} else {
-  let component = [FormAutoComplete];
-  this.NSGetFactory = XPCOMUtils.generateNSGetFactory(component);
-}
+this.NSGetFactory = XPCOMUtils.generateNSGetFactory([FormAutoComplete]);