Bug 1375568 - [Form Autofill] Improve the performance of the identifyAutofillFields process. r=MattN
- reduce many cross-JSM function calls
- reduce access to DOM element's properties
- cache the preferences
- avoid installing redundant event listeners
- remove logs
MozReview-Commit-ID: EwAu3amlFiX
--- a/browser/extensions/formautofill/FormAutofillContent.jsm
+++ b/browser/extensions/formautofill/FormAutofillContent.jsm
@@ -424,17 +424,22 @@ var FormAutofillContent = {
*
* @param {HTMLInputElement} element Focused input which triggered profile searching
* @returns {Array<Object>|null}
* Return target form's handler from content cache
* (or return null if the information is not found in the cache).
*
*/
getFormHandler(element) {
- let rootElement = FormLikeFactory.findRootForField(element);
+ // The algorithm to get the root element should align with
+ // FormLikeFactory.findRootForField().
+ let rootElement = element.form;
+ if (!rootElement) {
+ rootElement = element.ownerDocument.documentElement;
+ }
return this._formsDetails.get(rootElement);
},
/**
* Get the form's information from cache which is created after page identified.
*
* @param {HTMLInputElement} element Focused input which triggered profile searching
* @returns {Array<Object>|null}
@@ -448,53 +453,41 @@ var FormAutofillContent = {
},
getAllFieldNames(element) {
let formHandler = this.getFormHandler(element);
return formHandler ? formHandler.allFieldNames : null;
},
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");
}
let formHandler = this.getFormHandler(element);
if (!formHandler) {
let formLike = FormLikeFactory.createFromField(element);
formHandler = new FormAutofillHandler(formLike);
+ this._formsDetails.set(formHandler.form.rootElement, formHandler);
} else if (!formHandler.isFormChangedSinceLastCollection) {
- this.log.debug("No control is removed or inserted since last collection.");
return;
}
- formHandler.collectFormFields();
+ let {addressFieldDetails, creditCardFieldDetails} = formHandler.collectFormFields();
- this._formsDetails.set(formHandler.form.rootElement, formHandler);
- this.log.debug("Adding form handler to _formsDetails:", formHandler);
-
- if (formHandler.isValidAddressForm) {
- formHandler.addressFieldDetails.forEach(
+ if (addressFieldDetails) {
+ addressFieldDetails.forEach(
detail => this._markAsAutofillField(detail.elementWeakRef.get())
);
- } else {
- this.log.debug("Ignoring address related fields since it has only",
- formHandler.addressFieldDetails.length,
- "field(s)");
}
- if (formHandler.isValidCreditCardForm) {
- formHandler.creditCardFieldDetails.forEach(
+ if (creditCardFieldDetails) {
+ creditCardFieldDetails.forEach(
detail => this._markAsAutofillField(detail.elementWeakRef.get())
);
- } else {
- this.log.debug("Ignoring credit card related fields since it's without credit card number field");
}
},
_markAsAutofillField(field) {
// Since Form Autofill popup is only for input element, any non-Input
// element should be excluded here.
if (!field || !(field instanceof Ci.nsIDOMHTMLInputElement)) {
return;
--- a/browser/extensions/formautofill/FormAutofillHandler.jsm
+++ b/browser/extensions/formautofill/FormAutofillHandler.jsm
@@ -13,17 +13,19 @@ this.EXPORTED_SYMBOLS = ["FormAutofillHa
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/FormAutofillUtils.jsm");
const PREF_HEURISTICS_ENABLED = "extensions.formautofill.heuristics.enabled";
-const ALLOWED_TYPES = Array.from(FormAutofillUtils.ALLOWED_TYPES);
+// This list should align with the same one in FormAutofillFrameScript.js.
+const ALLOWED_TYPES = ["text", "email", "tel", "number"];
+
const FIELD_NAME_INFO = Object.assign({}, FormAutofillUtils.FIELD_NAME_INFO);
const AUTOFILL_FIELDS_THRESHOLD = FormAutofillUtils.AUTOFILL_FIELDS_THRESHOLD;
this.log = null;
FormAutofillUtils.defineLazyLogGetter(this, this.EXPORTED_SYMBOLS[0]);
/**
* Handles profile autofill for a DOM Form element.
@@ -106,30 +108,38 @@ FormAutofillHandler.prototype = {
// can be recognized as there is no element changed. However, we should
// improve the function to detect the element changes. e.g. a tel field
// is changed from type="hidden" to type="tel".
return this._formFieldCount != this.form.elements.length;
},
/**
* Set fieldDetails from the form about fields that can be autofilled.
+ *
+ * @returns {Object}
+ * An object containing addressFieldDetails and creditCardFieldDetails
+ * for the later use.
*/
collectFormFields() {
this._cacheValue.allFieldNames = null;
this._formFieldCount = this.form.elements.length;
let fieldDetails = FormAutofillHeuristics.getFormInfo(this.form);
this.fieldDetails = fieldDetails ? fieldDetails : [];
- log.debug("Collected details on", this.fieldDetails.length, "fields");
this.addressFieldDetails = this.fieldDetails.filter(
detail => this.isAddressField(detail.fieldName)
);
this.creditCardFieldDetails = this.fieldDetails.filter(
detail => this.isCreditCardField(detail.fieldName)
);
+
+ return {
+ addressFieldDetails: this.isValidAddressForm ? this.addressFieldDetails : null,
+ creditCardFieldDetails: this.isValidCreditCardForm ? this.creditCardFieldDetails : null,
+ };
},
getFieldDetailByName(fieldName) {
return this.fieldDetails.find(detail => detail.fieldName == fieldName);
},
_cacheValue: {
allFieldNames: null,
@@ -454,17 +464,16 @@ this.FormAutofillHeuristics = {
}
// Store the association between the field metadata and the element.
if (fieldDetails.some(f => f.section == info.section &&
f.addressType == info.addressType &&
f.contactType == info.contactType &&
f.fieldName == info.fieldName)) {
// A field with the same identifier already exists.
- log.debug("Not collecting a field matching another with the same info:", info);
continue;
}
let formatWithElement = {
section: info.section,
addressType: info.addressType,
contactType: info.contactType,
fieldName: info.fieldName,
@@ -523,28 +532,38 @@ this.FormAutofillHeuristics = {
if (this.RULES[fieldName].test(string)) {
result.fieldName = fieldName;
return result;
}
}
return null;
},
+ _isFieldEligibleForAutofill(element, autocomplete, type) {
+ if (autocomplete == "off") {
+ return false;
+ }
+
+ let tagName = element.tagName;
+ if (tagName == "INPUT") {
+ if (!ALLOWED_TYPES.includes(type)) {
+ return false;
+ }
+ } else if (tagName != "SELECT") {
+ return false;
+ }
+
+ return true;
+ },
+
getInfo(element, fieldDetails) {
let autocomplete = element.autocomplete;
- let tagName = element.tagName;
let type = element.type;
- if (autocomplete == "off") {
- return null;
- } else if (tagName == "INPUT") {
- if (!ALLOWED_TYPES.includes(type)) {
- return null;
- }
- } else if (tagName != "SELECT") {
+ if (!this._isFieldEligibleForAutofill(element, autocomplete, type)) {
return null;
}
// An input[autocomplete="on"] will not be early return here since it stll
// needs to find the field name.
if (autocomplete != "on") {
let info = element.getAutocompleteInfo();
if (info && info.fieldName) {
@@ -575,17 +594,16 @@ this.FormAutofillHeuristics = {
let fieldNameResult = this._matchStringToFieldName(elementString,
existingFieldNames);
if (fieldNameResult) {
return fieldNameResult;
}
}
let labels = this.findLabelElements(element);
if (!labels || labels.length == 0) {
- log.debug("No label found for", element);
return null;
}
for (let label of labels) {
let strings = this.extractLabelStrings(label);
for (let string of strings) {
let fieldNameResult = this._matchStringToFieldName(string,
existingFieldNames);
if (fieldNameResult) {
@@ -648,29 +666,27 @@ this.FormAutofillHeuristics = {
// should be refined after input.labels API landed.
for (let label of document.querySelectorAll("label[for]")) {
if (id == label.htmlFor) {
labels.push(label);
}
}
if (labels.length > 0) {
- log.debug("Label found by ID", id);
return labels;
}
let parent = element.parentNode;
if (!parent) {
return [];
}
do {
if (parent.tagName == "LABEL" &&
parent.control == element &&
!parent.hasAttribute("for")) {
- log.debug("Label found in input's parent or ancestor.");
return [parent];
}
parent = parent.parentNode;
} while (parent);
return [];
},
};
--- a/browser/extensions/formautofill/FormAutofillUtils.jsm
+++ b/browser/extensions/formautofill/FormAutofillUtils.jsm
@@ -87,34 +87,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,108 @@
"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 FormAutofillHandler.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;
+ },
+
+ _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 +125,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,20 @@
"use strict";
-Cu.import("resource://formautofill/FormAutofillUtils.jsm");
+Cu.import("resource://formautofill/FormAutofillHandler.jsm");
+
+const ScriptLoader = Cc["@mozilla.org/moz/jssubscript-loader;1"]
+ .getService(Ci.mozIJSSubScriptLoader);
+let sandbox = {
+ addEventListener() {},
+ addMessageListener() {},
+};
+ScriptLoader.loadSubScript("chrome://formautofill/content/FormAutofillFrameScript.js", sandbox, "utf-8");
+const {FormAutofillFrameScript} = sandbox;
const TESTCASES = [
{
document: `<input id="targetElement" type="text">`,
fieldId: "targetElement",
expectedResult: true,
},
{
@@ -68,12 +77,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(FormAutofillFrameScript._isFieldEligibleForAutofill(field),
+ testcase.expectedResult);
+ Assert.equal(FormAutofillHeuristics._isFieldEligibleForAutofill(field, field.autocomplete, field.type),
testcase.expectedResult);
});
});