Bug 1479740 - Track Web API calls made in the child - r?kmag
The performance counter is now also used in the children, and
the ParentAPIManager.retrievePerformanceCounters() can be used
to aggregate all counters into a promise.
MozReview-Commit-ID: KqzMVfdOTYP
--- a/modules/libpref/init/all.js
+++ b/modules/libpref/init/all.js
@@ -5001,16 +5001,21 @@ pref("extensions.webextensions.backgroun
pref("extensions.webextensions.ExtensionStorageIDB.enabled", true);
#else
pref("extensions.webextensions.ExtensionStorageIDB.enabled", false);
#endif
// if enabled, store execution times for API calls
pref("extensions.webextensions.enablePerformanceCounters", false);
+// Maximum age in milliseconds of performance counters in children
+// When reached, the counters are sent to the main process and
+// reset, so we reduce memory footprint.
+pref("extensions.webextensions.performanceCountersMaxAge", 0);
+
// Report Site Issue button
pref("extensions.webcompat-reporter.newIssueEndpoint", "https://webcompat.com/issues/new");
#if defined(MOZ_DEV_EDITION) || defined(NIGHTLY_BUILD)
pref("extensions.webcompat-reporter.enabled", true);
#else
pref("extensions.webcompat-reporter.enabled", false);
#endif
--- a/toolkit/components/extensions/ExtensionChild.jsm
+++ b/toolkit/components/extensions/ExtensionChild.jsm
@@ -19,20 +19,22 @@ var EXPORTED_SYMBOLS = ["ExtensionChild"
ChromeUtils.import("resource://gre/modules/Services.jsm");
ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
XPCOMUtils.defineLazyServiceGetter(this, "finalizationService",
"@mozilla.org/toolkit/finalizationwitness;1",
"nsIFinalizationWitnessService");
XPCOMUtils.defineLazyModuleGetters(this, {
+ DeferredTask: "resource://gre/modules/DeferredTask.jsm",
ExtensionContent: "resource://gre/modules/ExtensionContent.jsm",
ExtensionPageChild: "resource://gre/modules/ExtensionPageChild.jsm",
MessageChannel: "resource://gre/modules/MessageChannel.jsm",
NativeApp: "resource://gre/modules/NativeMessaging.jsm",
+ PerformanceCounters: "resource://gre/modules/PerformanceCounters.jsm",
PromiseUtils: "resource://gre/modules/PromiseUtils.jsm",
});
XPCOMUtils.defineLazyGetter(
this, "processScript",
() => Cc["@mozilla.org/webextensions/extension-process-script;1"]
.getService().wrappedJSObject);
@@ -850,16 +852,68 @@ class ProxyAPIImplementation extends Sch
}
hasListener(listener) {
let map = this.childApiManager.listeners.get(this.path);
return map.listeners.has(listener);
}
}
+class ChildLocalAPIImplementation extends LocalAPIImplementation {
+ constructor(pathObj, name, childApiManager) {
+ super(pathObj, name, childApiManager.context);
+ this.childApiManager = childApiManager;
+ this._purger = null;
+ }
+
+ sendPerformanceCounter() {
+ if (!this.childApiManager.context.active) {
+ return;
+ }
+ let options = {childId: this.childApiManager.id,
+ counters: PerformanceCounters.flush()};
+ Services.cpmm.sendAsyncMessage("Extension:SendPerformanceCounter", options);
+ }
+
+ withTiming(callable) {
+ if (!PerformanceCounters.enabled) {
+ return callable();
+ }
+ let start = Cu.now();
+ try {
+ return callable();
+ } finally {
+ let end = Cu.now();
+ PerformanceCounters.storeExecutionTime(this.context.extension.id, this.name, end - start);
+ // If we store a performance counter for the first time, we create this._purger.
+ // If this._purger already exists and is not armed, we arm it.
+ if (!this._purger) {
+ this._purger = new DeferredTask(() => {
+ this.sendPerformanceCounter();
+ }, PerformanceCounters.maxAge);
+ this._purger.arm();
+ } else if (!this._purger.isArmed) {
+ this._purger.arm();
+ }
+ }
+ }
+
+ callFunction(args) {
+ return this.withTiming(() => super.callFunction(args));
+ }
+
+ callFunctionNoReturn(args) {
+ return this.withTiming(() => super.callFunctionNoReturn(args));
+ }
+
+ callAsyncFunction(args, callback, requireUserInput) {
+ return this.withTiming(() => super.callAsyncFunction(args, callback, requireUserInput));
+ }
+}
+
// We create one instance of this class for every extension context that
// needs to use remote APIs. It uses the message manager to communicate
// with the ParentAPIManager singleton in ExtensionParent.jsm. It
// handles asynchronous function calls as well as event listeners.
class ChildAPIManager {
constructor(context, messageManager, localAPICan, contextData) {
this.context = context;
this.messageManager = messageManager;
@@ -1067,17 +1121,17 @@ class ChildAPIManager {
return true;
}
getImplementation(namespace, name) {
this.apiCan.findAPIPath(`${namespace}.${name}`);
let obj = this.apiCan.findAPIPath(namespace);
if (obj && name in obj) {
- return new LocalAPIImplementation(obj, name, this.context);
+ return new ChildLocalAPIImplementation(obj, name, this);
}
return this.getFallbackImplementation(namespace, name);
}
getFallbackImplementation(namespace, name) {
// No local API found, defer implementation to the parent.
return new ProxyAPIImplementation(namespace, name, this);
--- a/toolkit/components/extensions/ExtensionParent.jsm
+++ b/toolkit/components/extensions/ExtensionParent.jsm
@@ -23,16 +23,17 @@ XPCOMUtils.defineLazyModuleGetters(this,
AsyncShutdown: "resource://gre/modules/AsyncShutdown.jsm",
DeferredTask: "resource://gre/modules/DeferredTask.jsm",
E10SUtils: "resource://gre/modules/E10SUtils.jsm",
ExtensionData: "resource://gre/modules/Extension.jsm",
MessageChannel: "resource://gre/modules/MessageChannel.jsm",
MessageManagerProxy: "resource://gre/modules/MessageManagerProxy.jsm",
NativeApp: "resource://gre/modules/NativeMessaging.jsm",
OS: "resource://gre/modules/osfile.jsm",
+ PerformanceCounters: "resource://gre/modules/PerformanceCounters.jsm",
PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.jsm",
Schemas: "resource://gre/modules/Schemas.jsm",
});
XPCOMUtils.defineLazyServiceGetters(this, {
aomStartup: ["@mozilla.org/addons/addon-manager-startup;1", "amIAddonManagerStartup"],
});
@@ -55,17 +56,16 @@ var {
promiseEvent,
promiseObserved,
} = ExtensionUtils;
const BASE_SCHEMA = "chrome://extensions/content/schemas/manifest.json";
const CATEGORY_EXTENSION_MODULES = "webextension-modules";
const CATEGORY_EXTENSION_SCHEMAS = "webextension-schemas";
const CATEGORY_EXTENSION_SCRIPTS = "webextension-scripts";
-const TIMING_ENABLED_PREF = "extensions.webextensions.enablePerformanceCounters";
let schemaURLs = new Set();
schemaURLs.add("chrome://extensions/content/schemas/experiments.json");
let GlobalManager;
let ParentAPIManager;
let ProxyMessenger;
@@ -502,27 +502,37 @@ GlobalManager = {
init(extension) {
if (this.extensionMap.size == 0) {
ProxyMessenger.init();
apiManager.on("extension-browser-inserted", this._onExtensionBrowser);
this.initialized = true;
}
this.extensionMap.set(extension.id, extension);
+ Services.ppmm.addMessageListener("Extension:SendPerformanceCounter", this);
},
uninit(extension) {
+ Services.ppmm.removeMessageListener("Extension:SendPerformanceCounter", this);
this.extensionMap.delete(extension.id);
if (this.extensionMap.size == 0 && this.initialized) {
apiManager.off("extension-browser-inserted", this._onExtensionBrowser);
this.initialized = false;
}
},
+ async receiveMessage({name, data}) {
+ switch (name) {
+ case "Extension:SendPerformanceCounter":
+ PerformanceCounters.merge(data.counters);
+ break;
+ }
+ },
+
_onExtensionBrowser(type, browser, additionalData = {}) {
browser.messageManager.loadFrameScript(`data:,
Components.utils.import("resource://gre/modules/Services.jsm");
Services.obs.notifyObservers(this, "tab-content-frameloader-created", "");
`, false);
let viewType = browser.getAttribute("webextension-view-type");
@@ -744,30 +754,26 @@ class DevToolsExtensionPageContextParent
this._devToolsToolbox = null;
super.shutdown();
}
}
ParentAPIManager = {
- // stores dispatches counts per web extension and API
- performanceCounters: new DefaultMap(() => new DefaultMap(() => ({duration: 0, calls: 0}))),
-
proxyContexts: new Map(),
init() {
Services.obs.addObserver(this, "message-manager-close");
Services.mm.addMessageListener("API:CreateProxyContext", this);
Services.mm.addMessageListener("API:CloseProxyContext", this, true);
Services.mm.addMessageListener("API:Call", this);
Services.mm.addMessageListener("API:AddListener", this);
Services.mm.addMessageListener("API:RemoveListener", this);
- XPCOMUtils.defineLazyPreferenceGetter(this, "_timingEnabled", TIMING_ENABLED_PREF, false);
},
attachMessageManager(extension, processMessageManager) {
extension.parentMessageManager = processMessageManager;
},
async observe(subject, topic, data) {
if (topic === "message-manager-close") {
@@ -875,33 +881,32 @@ ParentAPIManager = {
closeProxyContext(childId) {
let context = this.proxyContexts.get(childId);
if (context) {
context.unload();
this.proxyContexts.delete(childId);
}
},
- storeExecutionTime(webExtensionId, apiPath, duration) {
- let apiCounter = this.performanceCounters.get(webExtensionId).get(apiPath);
- apiCounter.duration += duration;
- apiCounter.calls += 1;
+ async retrievePerformanceCounters() {
+ // getting the parent counters
+ return PerformanceCounters.getData();
},
async withTiming(data, callable) {
- if (!this._timingEnabled) {
+ if (!PerformanceCounters.enabled) {
return callable();
}
let webExtId = data.childId.split(".")[0];
let start = Cu.now();
try {
return callable();
} finally {
let end = Cu.now();
- this.storeExecutionTime(webExtId, data.path, end - start);
+ PerformanceCounters.storeExecutionTime(webExtId, data.path, end - start);
}
},
async call(data, target) {
let context = this.getContextById(data.childId);
if (context.parentMessageManager !== target.messageManager) {
throw new Error("Got message on unexpected message manager");
}
new file mode 100644
--- /dev/null
+++ b/toolkit/components/extensions/PerformanceCounters.jsm
@@ -0,0 +1,126 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+/* 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/. */
+"use strict";
+
+/**
+ * This module contains a global counter to store API call in the current process.
+ */
+
+/* exported Counters */
+var EXPORTED_SYMBOLS = ["PerformanceCounters"];
+
+ChromeUtils.import("resource://gre/modules/ExtensionUtils.jsm");
+ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
+
+const {
+ DefaultMap,
+} = ExtensionUtils;
+
+XPCOMUtils.defineLazyPreferenceGetter(this, "gTimingEnabled",
+ "extensions.webextensions.enablePerformanceCounters",
+ false);
+XPCOMUtils.defineLazyPreferenceGetter(this, "gTimingMaxAge",
+ "extensions.webextensions.performanceCountersMaxAge",
+ 0);
+
+class CounterMap extends DefaultMap {
+ defaultConstructor() {
+ return new DefaultMap(() => ({duration: 0, calls: 0}));
+ }
+}
+
+class Counters {
+ constructor() {
+ this.data = new CounterMap();
+ }
+
+ /**
+ * Returns true if performance counters are enabled.
+ *
+ * Indirection used so gTimingEnabled is not exposed direcly
+ * in PerformanceCounters -- which would prevent tests to dynamically
+ * change the preference value once PerformanceCounters.jsm is loaded.
+ *
+ * @returns {boolean}
+ */
+ get enabled() {
+ return gTimingEnabled;
+ }
+
+ /**
+ * Returns the counters max age
+ *
+ * Indirection used so gTimingMaxAge is not exposed direcly
+ * in PerformanceCounters -- which would prevent tests to dynamically
+ * change the preference value once PerformanceCounters.jsm is loaded.
+ *
+ * @returns {number}
+ */
+ get maxAge() {
+ return gTimingMaxAge;
+ }
+
+ /**
+ * Stores an execution time.
+ *
+ * @param {string} webExtensionId The web extension id.
+ * @param {string} apiPath The API path.
+ * @param {integer} duration How long the call took.
+ */
+ storeExecutionTime(webExtensionId, apiPath, duration) {
+ let apiCounter = this.data.get(webExtensionId).get(apiPath);
+ apiCounter.duration += duration;
+ apiCounter.calls += 1;
+ }
+
+ /**
+ * Merges another CounterMap into this.data
+ *
+ * Can be used by the main process to merge data received
+ * from the children.
+ *
+ * @param {CounterMap} data The map to merge.
+ */
+ merge(data) {
+ for (let [webextId, counters] of data) {
+ for (let [api, counter] of counters) {
+ let current = this.data.get(webextId).get(api);
+ current.calls += counter.calls;
+ current.duration += counter.duration;
+ }
+ }
+ }
+
+ /**
+ * Returns the performance counters and purges them.
+ *
+ * @returns {CounterMap}
+ */
+ flush() {
+ let result = new CounterMap();
+ for (let [webextId, counters] of this.data) {
+ for (let [api, counter] of counters) {
+ let current = result.get(webextId).get(api);
+ current.calls += counter.calls;
+ current.duration += counter.duration;
+ }
+ }
+ this.data = new CounterMap();
+ return result;
+ }
+
+ /**
+ * Returns the performance counters.
+ *
+ * @returns {CounterMap}
+ */
+ getData() {
+ return this.data;
+ }
+}
+
+
+var PerformanceCounters = new Counters();
--- a/toolkit/components/extensions/moz.build
+++ b/toolkit/components/extensions/moz.build
@@ -24,16 +24,17 @@ EXTRA_JS_MODULES += [
'ExtensionStorageSync.jsm',
'ExtensionUtils.jsm',
'FindContent.jsm',
'LegacyExtensionsUtils.jsm',
'MessageChannel.jsm',
'MessageManagerProxy.jsm',
'NativeManifests.jsm',
'NativeMessaging.jsm',
+ 'PerformanceCounters.jsm',
'ProxyScriptContext.jsm',
'Schemas.jsm',
]
EXTRA_COMPONENTS += [
'extension-process-script.js',
'extensions-toolkit.manifest',
]
--- a/toolkit/components/extensions/test/xpcshell/test_ext_performance_counters.js
+++ b/toolkit/components/extensions/test/xpcshell/test_ext_performance_counters.js
@@ -1,43 +1,103 @@
/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim: set sts=2 sw=2 et tw=80: */
"use strict";
ChromeUtils.import("resource://gre/modules/ExtensionParent.jsm");
+ChromeUtils.import("resource://gre/modules/PerformanceCounters.jsm");
+ChromeUtils.import("resource://gre/modules/Promise.jsm");
+
const ENABLE_COUNTER_PREF = "extensions.webextensions.enablePerformanceCounters";
+const TIMING_MAX_AGE = "extensions.webextensions.performanceCountersMaxAge";
let {
ParentAPIManager,
} = ExtensionParent;
+/*
+ * Monkey-patching PerformanceCounters so we know when data gets back from
+ * the child.
+ */
+let CounterObserver = {
+ init(apiName, expectedCount) {
+ this.old_merge = PerformanceCounters.merge;
+ PerformanceCounters.merge = this.merge;
+ PerformanceCounters.received = new Promise(resolve => {
+ PerformanceCounters.resolve = resolve;
+ });
+ PerformanceCounters.currentCount = 0;
+ PerformanceCounters.expectedCount = expectedCount;
+ PerformanceCounters.apiName = apiName;
+ },
+
+ uninit() {
+ PerformanceCounters.merge = this.old_merge;
+ },
+
+ async waitForCounters() {
+ return PerformanceCounters.received;
+ },
+
+ merge(data) {
+ let original = PerformanceCounters.getData();
+ for (let [webextId, counters] of data) {
+ for (let [api, counter] of counters) {
+ if (api == this.apiName) {
+ this.currentCount += counter.calls;
+ }
+ let current = original.get(webextId).get(api);
+ current.calls += counter.calls;
+ current.duration += counter.duration;
+ }
+ }
+ if (this.currentCount >= this.expectedCount) {
+ this.resolve();
+ }
+ },
+};
async function test_counter() {
async function background() {
- // creating a bookmark
+ // creating a bookmark is done in the parent
let folder = await browser.bookmarks.create({title: "Folder"});
await browser.bookmarks.create({title: "Bookmark", url: "http://example.com",
parentId: folder.id});
+
+ // getURL() is done in the child, let do three
+ browser.extension.getURL("beasts/frog.html");
+ browser.extension.getURL("beasts/frog2.html");
+ browser.extension.getURL("beasts/frog3.html");
browser.test.sendMessage("done");
}
let extensionData = {
background,
manifest: {
permissions: ["bookmarks"],
},
};
let extension = ExtensionTestUtils.loadExtension(extensionData);
+ CounterObserver.init("getURL", 3);
await extension.startup();
await extension.awaitMessage("done");
+ await CounterObserver.waitForCounters();
+ CounterObserver.uninit();
+
+ let counters = await ParentAPIManager.retrievePerformanceCounters();
await extension.unload();
// check that the bookmarks.create API was tracked
- let counters = ParentAPIManager.performanceCounters;
let counter = counters.get(extension.id).get("bookmarks.create");
ok(counter.calls > 0);
ok(counter.duration > 0);
+
+ // check that the getURL API was tracked
+ counter = counters.get(extension.id).get("getURL");
+ ok(counter.calls > 0);
+ ok(counter.duration > 0);
}
add_task(function test_performance_counter() {
- return runWithPrefs([[ENABLE_COUNTER_PREF, true]], test_counter);
+ return runWithPrefs([[ENABLE_COUNTER_PREF, true],
+ [TIMING_MAX_AGE, 1]], test_counter);
});