Bug 1392975 - [Form Autofill] Improve the performance before calling identifyAutofillFields. r=seanlee draft
authorLuke Chang <lchang@mozilla.com>
Wed, 23 Aug 2017 17:35:32 +0800
changeset 651180 4f8975e1e6545e9a8f9f28ad7cee74092fcaa83c
parent 650941 7c50f0c999c5bf8ee915261997597a5a9b8fb2ae
child 727611 38894f8825d88dc376a38b3617133b45803228a6
push id75621
push userbmo:lchang@mozilla.com
push dateWed, 23 Aug 2017 09:48:14 +0000
reviewersseanlee
bugs1392975
milestone57.0a1
Bug 1392975 - [Form Autofill] Improve the performance before calling identifyAutofillFields. r=seanlee MozReview-Commit-ID: Eo3KSBoaotr
browser/extensions/formautofill/FormAutofillContent.jsm
browser/extensions/formautofill/FormAutofillHeuristics.jsm
browser/extensions/formautofill/FormAutofillUtils.jsm
browser/extensions/formautofill/content/FormAutofillFrameScript.js
browser/extensions/formautofill/test/unit/test_isFieldEligibleForAutofill.js
--- a/browser/extensions/formautofill/FormAutofillContent.jsm
+++ b/browser/extensions/formautofill/FormAutofillContent.jsm
@@ -469,21 +469,16 @@ var FormAutofillContent = {
   identifyAutofillFields(element) {
     this.log.debug("identifyAutofillFields:", "" + element.ownerDocument.location);
 
     if (!this.savedFieldNames) {
       this.log.debug("identifyAutofillFields: savedFieldNames are not known yet");
       Services.cpmm.sendAsyncMessage("FormAutofill:InitStorage");
     }
 
-    if (!FormAutofillUtils.isFieldEligibleForAutofill(element)) {
-      this.log.debug("Not an eligible field.");
-      return;
-    }
-
     let formHandler = this.getFormHandler(element);
     if (!formHandler) {
       let formLike = FormLikeFactory.createFromField(element);
       formHandler = new FormAutofillHandler(formLike);
     } else if (!formHandler.isFormChangedSinceLastCollection) {
       this.log.debug("No control is removed or inserted since last collection.");
       return;
     }
--- a/browser/extensions/formautofill/FormAutofillHeuristics.jsm
+++ b/browser/extensions/formautofill/FormAutofillHeuristics.jsm
@@ -283,16 +283,49 @@ this.LabelUtils = {
 };
 
 /**
  * Returns the autocomplete information of fields according to heuristics.
  */
 this.FormAutofillHeuristics = {
   RULES: null,
 
+  // This list should align with the same one in FormAutofillFrameScript.js.
+  ALLOWED_TYPES: ["text", "email", "tel", "number"],
+
+  /**
+   * Determine whether an element is eligible for applying autofill feature.
+   *
+   * NOTE:
+   * This function should align with the same one in FormAutofillFrameScript.js.
+   *
+   * @param   {HTMLElement} element
+   *          The element to be determined.
+   * @returns {boolean}
+   *          Whether the element is eligible.
+   */
+  _isFieldEligibleForAutofill(element) {
+    let autocomplete = element.autocomplete;
+
+    if (autocomplete == "off") {
+      return false;
+    }
+
+    let tagName = element.tagName;
+    if (tagName == "INPUT") {
+      if (!this.ALLOWED_TYPES.includes(element.type)) {
+        return false;
+      }
+    } else if (tagName != "SELECT") {
+      return false;
+    }
+
+    return true;
+  },
+
   /**
    * Try to match the telephone related fields to the grammar
    * list to see if there is any valid telephone set and correct their
    * field names.
    *
    * @param {FieldScanner} fieldScanner
    *        The current parsing status for all elements
    * @returns {boolean}
@@ -430,17 +463,17 @@ this.FormAutofillHeuristics = {
     if (allowDuplicates) {
       return fieldScanner.fieldDetails;
     }
 
     return fieldScanner.trimmedFieldDetail;
   },
 
   getInfo(element) {
-    if (!FormAutofillUtils.isFieldEligibleForAutofill(element)) {
+    if (!this._isFieldEligibleForAutofill(element)) {
       return null;
     }
 
     let info = element.getAutocompleteInfo();
     // An input[autocomplete="on"] will not be early return here since it stll
     // needs to find the field name.
     if (info && info.fieldName && info.fieldName != "on") {
       info._reason = "autocomplete";
--- a/browser/extensions/formautofill/FormAutofillUtils.jsm
+++ b/browser/extensions/formautofill/FormAutofillUtils.jsm
@@ -105,34 +105,16 @@ this.FormAutofillUtils = {
       });
     });
   },
 
   autofillFieldSelector(doc) {
     return doc.querySelectorAll("input, select");
   },
 
-  ALLOWED_TYPES: ["text", "email", "tel", "number"],
-  isFieldEligibleForAutofill(element) {
-    if (element.autocomplete == "off") {
-      return false;
-    }
-
-    if (element instanceof Ci.nsIDOMHTMLInputElement) {
-      // `element.type` can be recognized as `text`, if it's missing or invalid.
-      if (!this.ALLOWED_TYPES.includes(element.type)) {
-        return false;
-      }
-    } else if (!(element instanceof Ci.nsIDOMHTMLSelectElement)) {
-      return false;
-    }
-
-    return true;
-  },
-
   loadDataFromScript(url, sandbox = {}) {
     let scriptLoader = Cc["@mozilla.org/moz/jssubscript-loader;1"]
                          .getService(Ci.mozIJSSubScriptLoader);
     scriptLoader.loadSubScript(url, sandbox, "utf-8");
     return sandbox;
   },
 
   /**
--- a/browser/extensions/formautofill/content/FormAutofillFrameScript.js
+++ b/browser/extensions/formautofill/content/FormAutofillFrameScript.js
@@ -8,66 +8,109 @@
 
 "use strict";
 
 /* eslint-env mozilla/frame-script */
 
 const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
 
 Cu.import("resource://gre/modules/Services.jsm");
-Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.import("resource://formautofill/FormAutofillContent.jsm");
-Cu.import("resource://formautofill/FormAutofillUtils.jsm");
+
+const PREF_ADDRESSES_ENABLED = "extensions.formautofill.addresses.enabled";
+
+// This list should align with the same one in FormAutofillHeuristics.jsm.
+const ALLOWED_TYPES = ["text", "email", "tel", "number"];
 
 /**
  * Handles content's interactions for the frame.
  *
  * NOTE: Declares it by "var" to make it accessible in unit tests.
  */
 var FormAutofillFrameScript = {
+  _nextHandleElement: null,
+  _alreadyDCL: false,
+  _hasDCLhandler: false,
+  _hasPendingTask: false,
+
+  get _prefEnabled() {
+    if (this.__prefEnabled === undefined) {
+      this.__prefEnabled = Services.prefs.getBoolPref(PREF_ADDRESSES_ENABLED);
+    }
+    return this.__prefEnabled;
+  },
+
+  // This function should align with the same one in FormAutofillHeuristics.jsm.
+  _isFieldEligibleForAutofill(element) {
+    let autocomplete = element.autocomplete;
+
+    if (autocomplete == "off") {
+      return false;
+    }
+
+    let tagName = element.tagName;
+    if (tagName == "INPUT") {
+      if (!ALLOWED_TYPES.includes(element.type)) {
+        return false;
+      }
+    } else if (tagName != "SELECT") {
+      return false;
+    }
+
+    return true;
+  },
+
+  _doIdentifyAutofillFields() {
+    if (this._hasPendingTask) {
+      return;
+    }
+    this._hasPendingTask = true;
+
+    setTimeout(() => {
+      FormAutofillContent.identifyAutofillFields(this._nextHandleElement);
+      this._hasPendingTask = false;
+      this._nextHandleElement = null;
+    });
+  },
+
   init() {
     addEventListener("focusin", this);
     addMessageListener("FormAutofill:PreviewProfile", this);
     addMessageListener("FormAutoComplete:PopupClosed", this);
     addMessageListener("FormAutoComplete:PopupOpened", this);
   },
 
   handleEvent(evt) {
-    if (!evt.isTrusted) {
-      return;
-    }
-
-    if (!Services.prefs.getBoolPref("extensions.formautofill.addresses.enabled")) {
+    if (!evt.isTrusted || !this._prefEnabled) {
       return;
     }
 
-    switch (evt.type) {
-      case "focusin": {
-        let element = evt.target;
-        let doc = element.ownerDocument;
-
-        if (!FormAutofillUtils.isFieldEligibleForAutofill(element)) {
-          return;
-        }
+    let element = evt.target;
+    if (!this._isFieldEligibleForAutofill(element)) {
+      return;
+    }
+    this._nextHandleElement = element;
 
-        let doIdentifyAutofillFields =
-          () => setTimeout(() => FormAutofillContent.identifyAutofillFields(element));
+    if (!this._alreadyDCL) {
+      let doc = element.ownerDocument;
+      if (doc.readyState === "loading") {
+        if (!this._hasDCLhandler) {
+          this._hasDCLhandler = true;
+          doc.addEventListener("DOMContentLoaded", () => this._doIdentifyAutofillFields(), {once: true});
+        }
+        return;
+      }
+      this._alreadyDCL = true;
+    }
 
-        if (doc.readyState === "loading") {
-          doc.addEventListener("DOMContentLoaded", doIdentifyAutofillFields, {once: true});
-        } else {
-          doIdentifyAutofillFields();
-        }
-        break;
-      }
-    }
+    this._doIdentifyAutofillFields();
   },
 
   receiveMessage(message) {
-    if (!Services.prefs.getBoolPref("extensions.formautofill.addresses.enabled")) {
+    if (!this._prefEnabled) {
       return;
     }
 
     const doc = content.document;
     const {chromeEventHandler} = doc.ownerGlobal.getInterface(Ci.nsIDocShell);
 
     switch (message.name) {
       case "FormAutofill:PreviewProfile": {
@@ -83,9 +126,13 @@ var FormAutofillFrameScript = {
       case "FormAutoComplete:PopupOpened": {
         chromeEventHandler.addEventListener("keydown", FormAutofillContent._onKeyDown,
                                             {capturing: true});
       }
     }
   },
 };
 
+Services.prefs.addObserver(PREF_ADDRESSES_ENABLED, () => {
+  delete FormAutofillFrameScript.__prefEnabled;
+});
+
 FormAutofillFrameScript.init();
--- a/browser/extensions/formautofill/test/unit/test_isFieldEligibleForAutofill.js
+++ b/browser/extensions/formautofill/test/unit/test_isFieldEligibleForAutofill.js
@@ -1,11 +1,18 @@
 "use strict";
 
-Cu.import("resource://formautofill/FormAutofillUtils.jsm");
+Cu.import("resource://formautofill/FormAutofillHeuristics.jsm");
+
+// Load FormAutofillFrameScript.js into a sandbox to be able to test `_isFieldEligibleForAutofill`
+let sandbox = {
+  addEventListener() {},
+  addMessageListener() {},
+};
+Services.scriptloader.loadSubScript("chrome://formautofill/content/FormAutofillFrameScript.js", sandbox, "utf-8");
 
 const TESTCASES = [
   {
     document: `<input id="targetElement" type="text">`,
     fieldId: "targetElement",
     expectedResult: true,
   },
   {
@@ -68,12 +75,14 @@ const TESTCASES = [
 TESTCASES.forEach(testcase => {
   add_task(async function() {
     do_print("Starting testcase: " + testcase.document);
 
     let doc = MockDocument.createTestDocument(
       "http://localhost:8080/test/", testcase.document);
 
     let field = doc.getElementById(testcase.fieldId);
-    Assert.equal(FormAutofillUtils.isFieldEligibleForAutofill(field),
+    Assert.equal(FormAutofillHeuristics._isFieldEligibleForAutofill(field),
+                 testcase.expectedResult);
+    Assert.equal(sandbox.FormAutofillFrameScript._isFieldEligibleForAutofill(field),
                  testcase.expectedResult);
   });
 });