Bug 1446250: Part 1 - Optimize Photon PageAction update performance. r=Gijs

The amount of computational complexity and garbage array/string/object
generation for each update to a pageAction property went up astronomically
with the migration of WebExtension page actions to the Photon API. This
resulted in non-trivial talos regression when Screenshots attempted to switch
back to the built-in pageAction API.

These changes fix most of the garbage generation, and reduce a lot of the
duplicated work for each update.

MozReview-Commit-ID: 4uPLnAesdU2

--HG--
extra : rebase_source : 3f723f3f35abf032cf12e02ce38552e21ea4827f
This commit is contained in:
Kris Maglione 2018-03-15 21:34:01 -07:00
Родитель 57a55d1c2d
Коммит eb1a0bb258
2 изменённых файлов: 169 добавлений и 106 удалений

Просмотреть файл

@ -462,28 +462,36 @@ var BrowserPageActions = {
* @param propertyName (string, optional)
* The name of the property to update. If not given, then DOM nodes
* will be updated to reflect the current values of all properties.
* @param value (optional)
* If a property name is passed, this argument may contain its
* current value, in order to prevent a further look-up.
*/
updateAction(action, propertyName = null) {
let propertyNames = propertyName ? [propertyName] : [
"iconURL",
"title",
"tooltip",
];
for (let name of propertyNames) {
let upper = name[0].toUpperCase() + name.substr(1);
this[`_updateAction${upper}`](action);
updateAction(action, propertyName = null, value) {
if (propertyName) {
this[this._updateMethods[propertyName]](action, value);
} else {
for (let name of ["iconURL", "title", "tooltip"]) {
this[this._updateMethods[name]](action, value);
}
}
},
_updateActionDisabled(action) {
this._updateActionDisabledInPanel(action);
_updateMethods: {
disabled: "_updateActionDisabled",
iconURL: "_updateActionIconURL",
title: "_updateActionTitle",
tooltip: "_updateActionTooltip",
},
_updateActionDisabled(action, disabled) {
this._updateActionDisabledInPanel(action, disabled);
this.placeActionInUrlbar(action);
},
_updateActionDisabledInPanel(action) {
_updateActionDisabledInPanel(action, disabled = action.getDisabled(window)) {
let panelButton = this.panelButtonNodeForActionID(action.id);
if (panelButton) {
if (action.getDisabled(window)) {
if (disabled) {
panelButton.setAttribute("disabled", "true");
} else {
panelButton.removeAttribute("disabled");
@ -491,26 +499,21 @@ var BrowserPageActions = {
}
},
_updateActionIconURL(action) {
let nodes = [
this.panelButtonNodeForActionID(action.id),
this.urlbarButtonNodeForActionID(action.id),
].filter(n => !!n);
for (let node of nodes) {
for (let size of [16, 32]) {
let url = action.iconURLForSize(size, window);
let prop = `--pageAction-image-${size}px`;
if (url) {
node.style.setProperty(prop, `url("${url}")`);
} else {
node.style.removeProperty(prop);
}
_updateActionIconURL(action, properties = action.getIconProperties(window)) {
let panelButton = this.panelButtonNodeForActionID(action.id);
let urlbarButton = this.urlbarButtonNodeForActionID(action.id);
for (let [prop, value] of Object.entries(properties)) {
if (panelButton) {
panelButton.style.setProperty(prop, value);
}
if (urlbarButton) {
urlbarButton.style.setProperty(prop, value);
}
}
},
_updateActionTitle(action) {
let title = action.getTitle(window);
_updateActionTitle(action, title = action.getTitle(window)) {
if (!title) {
// `title` is a required action property, but the bookmark action's is an
// empty string since its actual title is set via
@ -518,25 +521,28 @@ var BrowserPageActions = {
// return is to ignore that empty title.
return;
}
let attrNamesByNodeFnName = {
panelButtonNodeForActionID: "label",
urlbarButtonNodeForActionID: "aria-label",
};
for (let [fnName, attrName] of Object.entries(attrNamesByNodeFnName)) {
let node = this[fnName](action.id);
if (node) {
node.setAttribute(attrName, title);
}
let panelButton = this.panelButtonNodeForActionID(action.id);
if (panelButton) {
panelButton.setAttribute("label", title);
}
let urlbarButton = this.urlbarButtonNodeForActionID(action.id);
if (urlbarButton) {
urlbarButton.setAttribute("aria-label", title);
// tooltiptext falls back to the title, so update it, too.
this._updateActionTooltip(action, undefined, title, urlbarButton);
}
// tooltiptext falls back to the title, so update it, too.
this._updateActionTooltip(action);
},
_updateActionTooltip(action) {
let node = this.urlbarButtonNodeForActionID(action.id);
_updateActionTooltip(action, tooltip = action.getTooltip(window),
title,
node = this.urlbarButtonNodeForActionID(action.id)) {
if (node) {
let tooltip = action.getTooltip(window) || action.getTitle(window);
node.setAttribute("tooltiptext", tooltip);
if (!tooltip && title === undefined) {
title = action.getTitle(window);
}
node.setAttribute("tooltiptext", tooltip || title);
}
},

Просмотреть файл

@ -32,6 +32,11 @@ const ACTION_ID_BUILT_IN_SEPARATOR = "builtInSeparator";
const PREF_PERSISTED_ACTIONS = "browser.pageActions.persistedActions";
const PERSISTED_ACTIONS_CURRENT_VERSION = 1;
// Escapes the given raw URL string, and returns an equivalent CSS url()
// value for it.
function escapeCSSURL(url) {
return `url("${url.replace(/[\\\s"]/g, encodeURIComponent)}")`;
}
var PageActions = {
/**
@ -617,6 +622,29 @@ function Action(options) {
if (this._subview) {
this._subview = new Subview(options.subview);
}
/**
* A cache of the pre-computed CSS variable values for a given icon
* URLs object, as passed to _createIconProperties.
*/
this._iconProperties = new WeakMap();
/**
* The global values for the action properties.
*/
this._globalProps = {
disabled: this._disabled,
iconURL: this._iconURL,
iconProps: this._createIconProperties(this._iconURL),
title: this._title,
tooltip: this._tooltip,
};
/**
* A mapping of window-specific action property objects, each of which
* derives from the _globalProps object.
*/
this._windowProps = new WeakMap();
}
Action.prototype = {
@ -661,7 +689,7 @@ Action.prototype = {
* The action's disabled state (bool, nonnull)
*/
getDisabled(browserWindow = null) {
return !!this._getProperty("disabled", browserWindow);
return !!this._getProperties(browserWindow).disabled;
},
setDisabled(value, browserWindow = null) {
return this._setProperty("disabled", !!value, browserWindow);
@ -672,17 +700,49 @@ Action.prototype = {
* (string or object, nullable)
*/
getIconURL(browserWindow = null) {
return this._getProperty("iconURL", browserWindow);
return this._getProperties(browserWindow).iconURL;
},
setIconURL(value, browserWindow = null) {
return this._setProperty("iconURL", value, browserWindow);
let props = this._getProperties(browserWindow, !!browserWindow);
props.iconURL = value;
props.iconProps = this._createIconProperties(value);
this._updateProperty("iconURL", props.iconProps, browserWindow);
return value;
},
/**
* The set of CSS variables which define the action's icons in various
* sizes. This is generated automatically from the iconURL property.
*/
getIconProperties(browserWindow = null) {
return this._getProperties(browserWindow).iconProps;
},
_createIconProperties(urls) {
if (urls && typeof urls == "object") {
let props = this._iconProperties.get(urls);
if (!props) {
props = Object.freeze({
"--pageAction-image-16px": escapeCSSURL(this._iconURLForSize(urls, 16)),
"--pageAction-image-32px": escapeCSSURL(this._iconURLForSize(urls, 32)),
});
this._iconProperties.set(urls, props);
}
return props;
}
return Object.freeze({
"--pageAction-image-16px": null,
"--pageAction-image-32px": urls ? escapeCSSURL(urls) : null,
});
},
/**
* The action's title (string, nonnull)
*/
getTitle(browserWindow = null) {
return this._getProperty("title", browserWindow);
return this._getProperties(browserWindow).title;
},
setTitle(value, browserWindow = null) {
return this._setProperty("title", value, browserWindow);
@ -692,7 +752,7 @@ Action.prototype = {
* The action's tooltip (string, nullable)
*/
getTooltip(browserWindow = null) {
return this._getProperty("tooltip", browserWindow);
return this._getProperties(browserWindow).tooltip;
},
setTooltip(value, browserWindow = null) {
return this._setProperty("tooltip", value, browserWindow);
@ -710,56 +770,45 @@ Action.prototype = {
* globally.
*/
_setProperty(name, value, browserWindow) {
if (!browserWindow) {
// Set the global state.
this[`_${name}`] = value;
} else {
// Set the per-window state.
let props = this._propertiesByBrowserWindow.get(browserWindow);
if (!props) {
props = {};
this._propertiesByBrowserWindow.set(browserWindow, props);
}
props[name] = value;
}
// This may be called before the action has been added.
if (PageActions.actionForID(this.id)) {
for (let bpa of allBrowserPageActions(browserWindow)) {
bpa.updateAction(this, name);
}
}
let props = this._getProperties(browserWindow, !!browserWindow);
props[name] = value;
this._updateProperty(name, value, browserWindow);
return value;
},
/**
* Gets a property, optionally for a particular browser window.
*
* @param name (string, required)
* The (non-underscored) name of the property.
* @param browserWindow (DOM window, optional)
* If given, then the property will be fetched from this window's
* state. If the property does not exist in the window's state, or if
* no window is given, then the global value is returned.
* @return The property value.
*/
_getProperty(name, browserWindow) {
if (browserWindow) {
// Try the per-window state.
let props = this._propertiesByBrowserWindow.get(browserWindow);
if (props && name in props) {
return props[name];
_updateProperty(name, value, browserWindow) {
// This may be called before the action has been added.
if (PageActions.actionForID(this.id)) {
for (let bpa of allBrowserPageActions(browserWindow)) {
bpa.updateAction(this, name, value);
}
}
// Fall back to the global state.
return this[`_${name}`];
},
// maps browser windows => object with properties for that window
get _propertiesByBrowserWindow() {
if (!this.__propertiesByBrowserWindow) {
this.__propertiesByBrowserWindow = new WeakMap();
/**
* Returns the properties object for the given window, if it exists,
* or the global properties object if no window-specific properties
* exist.
*
* @param {Window?} window
* The window for which to return the properties object, or
* null to return the global properties object.
* @param {bool} [forceWindowSpecific = false]
* If true, always returns a window-specific properties object.
* If a properties object does not exist for the given window,
* one is created and cached.
* @returns {object}
*/
_getProperties(window, forceWindowSpecific = false) {
let props = window && this._windowProps.get(window);
if (!props && forceWindowSpecific) {
props = Object.create(this._globalProps);
this._windowProps.set(window, props);
}
return this.__propertiesByBrowserWindow;
return props || this._globalProps;
},
/**
@ -813,25 +862,33 @@ Action.prototype = {
return iconURL;
}
if (typeof(iconURL) == "object") {
// This case is copied from ExtensionParent.jsm so that our image logic is
// the same, so that WebExtensions page action tests that deal with icons
// pass.
let bestSize = null;
if (iconURL[preferredSize]) {
bestSize = preferredSize;
} else if (iconURL[2 * preferredSize]) {
bestSize = 2 * preferredSize;
} else {
let sizes = Object.keys(iconURL)
.map(key => parseInt(key, 10))
.sort((a, b) => a - b);
bestSize = sizes.find(candidate => candidate > preferredSize) || sizes.pop();
}
return iconURL[bestSize];
return this._iconURLForSize(iconURL, preferredSize);
}
return null;
},
/**
* Selects the best matching icon from the given URLs object for the
* given preferred size, as described in {@see iconURLForSize}.
*/
_iconURLForSize(urls, preferredSize) {
// This case is copied from ExtensionParent.jsm so that our image logic is
// the same, so that WebExtensions page action tests that deal with icons
// pass.
let bestSize = null;
if (urls[preferredSize]) {
bestSize = preferredSize;
} else if (urls[2 * preferredSize]) {
bestSize = 2 * preferredSize;
} else {
let sizes = Object.keys(urls)
.map(key => parseInt(key, 10))
.sort((a, b) => a - b);
bestSize = sizes.find(candidate => candidate > preferredSize) || sizes.pop();
}
return urls[bestSize];
},
/**
* Performs the command for an action. If the action has an onCommand
* handler, then it's called. If the action has a subview or iframe, then a