/* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ // This file defines these globals on the window object. // Define them here so that ESLint can find them: /* globals MozXULElement, MozHTMLElement, MozElements */ "use strict"; // This is loaded into chrome windows with the subscript loader. Wrap in // a block to prevent accidentally leaking globals onto `window`. (() => { // Handle customElements.js being loaded as a script in addition to the subscriptLoader // from MainProcessSingleton, to handle pages that can open both before and after // MainProcessSingleton starts. See Bug 1501845. if (window.MozXULElement) { return; } const MozElements = {}; window.MozElements = MozElements; const { AppConstants } = ChromeUtils.importESModule( "resource://gre/modules/AppConstants.sys.mjs" ); const instrumentClasses = Services.env.get("MOZ_INSTRUMENT_CUSTOM_ELEMENTS"); const instrumentedClasses = instrumentClasses ? new Set() : null; const instrumentedBaseClasses = instrumentClasses ? new WeakSet() : null; // If requested, wrap the normal customElements.define to give us a chance // to modify the class so we can instrument function calls in local development: if (instrumentClasses) { let define = window.customElements.define; window.customElements.define = function(name, c, opts) { instrumentCustomElementClass(c); return define.call(this, name, c, opts); }; window.addEventListener( "load", () => { MozElements.printInstrumentation(true); }, { once: true, capture: true } ); } MozElements.printInstrumentation = function(collapsed) { let summaries = []; let totalCalls = 0; let totalTime = 0; for (let c of instrumentedClasses) { // Allow passing in something like MOZ_INSTRUMENT_CUSTOM_ELEMENTS=MozXULElement,Button to filter let includeClass = instrumentClasses == 1 || instrumentClasses .split(",") .some(n => c.name.toLowerCase().includes(n.toLowerCase())); let summary = c.__instrumentation_summary; if (includeClass && summary) { summaries.push(summary); totalCalls += summary.totalCalls; totalTime += summary.totalTime; } } if (summaries.length) { let groupName = `Instrumentation data for custom elements in ${document.documentURI}`; console[collapsed ? "groupCollapsed" : "group"](groupName); console.log( `Total function calls ${totalCalls} and total time spent inside ${totalTime.toFixed( 2 )}` ); for (let summary of summaries) { console.log(`${summary.name} (# instances: ${summary.instances})`); if (Object.keys(summary.data).length > 1) { console.table(summary.data); } } console.groupEnd(groupName); } }; function instrumentCustomElementClass(c) { // Climb up prototype chain to see if we inherit from a MozElement. // Keep track of classes to instrument, for example: // MozMenuCaption->MozMenuBase->BaseText->BaseControl->MozXULElement let inheritsFromBase = instrumentedBaseClasses.has(c); let classesToInstrument = [c]; let proto = Object.getPrototypeOf(c); while (proto) { classesToInstrument.push(proto); if (instrumentedBaseClasses.has(proto)) { inheritsFromBase = true; break; } proto = Object.getPrototypeOf(proto); } if (inheritsFromBase) { for (let c of classesToInstrument.reverse()) { instrumentIndividualClass(c); } } } function instrumentIndividualClass(c) { if (instrumentedClasses.has(c)) { return; } instrumentedClasses.add(c); let data = { instances: 0 }; function wrapFunction(name, fn) { return function() { if (!data[name]) { data[name] = { time: 0, calls: 0 }; } data[name].calls++; let n = performance.now(); let r = fn.apply(this, arguments); data[name].time += performance.now() - n; return r; }; } function wrapPropertyDescriptor(obj, name) { if (name == "constructor") { return; } let prop = Object.getOwnPropertyDescriptor(obj, name); if (prop.get) { prop.get = wrapFunction(` ${name}`, prop.get); } if (prop.set) { prop.set = wrapFunction(` ${name}`, prop.set); } if (prop.writable && prop.value && prop.value.apply) { prop.value = wrapFunction(name, prop.value); } Object.defineProperty(obj, name, prop); } // Handle static properties for (let name of Object.getOwnPropertyNames(c)) { wrapPropertyDescriptor(c, name); } // Handle instance properties for (let name of Object.getOwnPropertyNames(c.prototype)) { wrapPropertyDescriptor(c.prototype, name); } c.__instrumentation_data = data; Object.defineProperty(c, "__instrumentation_summary", { enumerable: false, configurable: false, get() { if (data.instances == 0) { return null; } let clonedData = JSON.parse(JSON.stringify(data)); delete clonedData.instances; let totalCalls = 0; let totalTime = 0; for (let d in clonedData) { let { time, calls } = clonedData[d]; time = parseFloat(time.toFixed(2)); totalCalls += calls; totalTime += time; clonedData[d]["time (ms)"] = time; delete clonedData[d].time; clonedData[d].timePerCall = parseFloat((time / calls).toFixed(4)); } let timePerCall = parseFloat((totalTime / totalCalls).toFixed(4)); totalTime = parseFloat(totalTime.toFixed(2)); // Add a spaced-out final row with summed up totals clonedData["\ntotals"] = { "time (ms)": `\n${totalTime}`, calls: `\n${totalCalls}`, timePerCall: `\n${timePerCall}`, }; return { instances: data.instances, data: clonedData, name: c.name, totalCalls, totalTime, }; }, }); } // The listener of DOMContentLoaded must be set on window, rather than // document, because the window can go away before the event is fired. // In that case, we don't want to initialize anything, otherwise we // may be leaking things because they will never be destroyed after. let gIsDOMContentLoaded = false; const gElementsPendingConnection = new Set(); window.addEventListener( "DOMContentLoaded", () => { gIsDOMContentLoaded = true; for (let element of gElementsPendingConnection) { try { if (element.isConnected) { element.isRunningDelayedConnectedCallback = true; element.connectedCallback(); } } catch (ex) { console.error(ex); } element.isRunningDelayedConnectedCallback = false; } gElementsPendingConnection.clear(); }, { once: true, capture: true } ); const gXULDOMParser = new DOMParser(); gXULDOMParser.forceEnableXULXBL(); MozElements.MozElementMixin = Base => { let MozElementBase = class extends Base { constructor() { super(); if (instrumentClasses) { let proto = this.constructor; while (proto && proto != Base) { proto.__instrumentation_data.instances++; proto = Object.getPrototypeOf(proto); } } } /* * A declarative way to wire up attribute inheritance and automatically generate * the `observedAttributes` getter. For example, if you returned: * { * ".foo": "bar,baz=bat" * } * * Then the base class will automatically return ["bar", "bat"] from `observedAttributes`, * and set up an `attributeChangedCallback` to pass those attributes down onto an element * matching the ".foo" selector. * * See the `inheritAttribute` function for more details on the attribute string format. * * @return {Object} */ static get inheritedAttributes() { return null; } static get flippedInheritedAttributes() { // Have to be careful here, if a subclass overrides inheritedAttributes // and its parent class is instantiated first, then reading // this._flippedInheritedAttributes on the child class will return the // computed value from the parent. We store it separately on each class // to ensure everything works correctly when inheritedAttributes is // overridden. if (!this.hasOwnProperty("_flippedInheritedAttributes")) { let { inheritedAttributes } = this; if (!inheritedAttributes) { this._flippedInheritedAttributes = null; } else { this._flippedInheritedAttributes = {}; for (let selector in inheritedAttributes) { let attrRules = inheritedAttributes[selector].split(","); for (let attrRule of attrRules) { let attrName = attrRule; let attrNewName = attrRule; let split = attrName.split("="); if (split.length == 2) { attrName = split[1]; attrNewName = split[0]; } if (!this._flippedInheritedAttributes[attrName]) { this._flippedInheritedAttributes[attrName] = []; } this._flippedInheritedAttributes[attrName].push([ selector, attrNewName, ]); } } } } return this._flippedInheritedAttributes; } /* * Generate this array based on `inheritedAttributes`, if any. A class is free to override * this if it needs to do something more complex or wants to opt out of this behavior. */ static get observedAttributes() { return Object.keys(this.flippedInheritedAttributes || {}); } /* * Provide default lifecycle callback for attribute changes that will inherit attributes * based on the static `inheritedAttributes` Object. This can be overridden by callers. */ attributeChangedCallback(name, oldValue, newValue) { if (oldValue === newValue || !this.initializedAttributeInheritance) { return; } let list = this.constructor.flippedInheritedAttributes[name]; if (list) { this.inheritAttribute(list, name); } } /* * After setting content, calling this will cache the elements from selectors in the * static `inheritedAttributes` Object. It'll also do an initial call to `this.inheritAttributes()`, * so in the simple case, this is the only function you need to call. * * This should be called any time the children that are inheriting attributes changes. For instance, * it's common in a connectedCallback to do something like: * * this.textContent = ""; * this.append(MozXULElement.parseXULToFragment(`