--- a/browser/app/profile/firefox.js
+++ b/browser/app/profile/firefox.js
@@ -65,16 +65,27 @@ pref("extensions.systemAddon.update.url"
// Disable screenshots for now, Shield will enable this.
pref("extensions.screenshots.system-disabled", true);
// Disable add-ons that are not installed by the user in all scopes by default.
// See the SCOPE constants in AddonManager.jsm for values to use here.
pref("extensions.autoDisableScopes", 15);
+// This is where the profiler WebExtension API will look for breakpad symbols.
+// NOTE: deliberately http right now since https://symbols.mozilla.org is not supported.
+pref("extensions.geckoProfiler.symbols.url", "http://symbols.mozilla.org/");
+pref("extensions.geckoProfiler.acceptedExtensionIds", "geckoprofiler@mozilla.com");
+#if defined(XP_LINUX) || defined (XP_MACOSX)
+pref("extensions.geckoProfiler.getSymbolRules", "localBreakpad,remoteBreakpad,nm");
+#else // defined(XP_WIN)
+pref("extensions.geckoProfiler.getSymbolRules", "localBreakpad,remoteBreakpad");
+#endif
+
+
// Add-on content security policies.
pref("extensions.webextensions.base-content-security-policy", "script-src 'self' https://* moz-extension: blob: filesystem: 'unsafe-eval' 'unsafe-inline'; object-src 'self' https://* moz-extension: blob: filesystem:;");
pref("extensions.webextensions.default-content-security-policy", "script-src 'self'; object-src 'self';");
// Require signed add-ons by default
pref("xpinstall.signatures.required", true);
pref("xpinstall.signatures.devInfoURL", "https://wiki.mozilla.org/Addons/Extension_Signing");
new file mode 100644
--- /dev/null
+++ b/browser/components/extensions/ParseSymbols-worker.js
@@ -0,0 +1,59 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+/* eslint-env worker */
+/* globals OS, ParseSymbols */
+
+"use strict";
+
+importScripts("resource://gre/modules/osfile.jsm");
+importScripts("resource:///modules/ParseSymbols.jsm");
+
+async function fetchSymbolFile(url) {
+ const response = await fetch(url);
+
+ if (!response.ok) {
+ throw new Error(`got error status ${response.status}`);
+ }
+
+ return response.text();
+}
+
+function parse(text) {
+ const syms = new Map();
+
+ // Lines look like this:
+ //
+ // PUBLIC 3fc74 0 test_public_symbol
+ //
+ // FUNC 40330 8e 0 test_func_symbol
+ const symbolRegex = /\nPUBLIC ([0-9a-f]+) [0-9a-f]+ (.*)|\nFUNC ([0-9a-f]+) [0-9a-f]+ [0-9a-f]+ (.*)/g;
+
+ let match;
+ let approximateLength = 0;
+ while ((match = symbolRegex.exec(text))) {
+ const [address0, symbol0, address1, symbol1] = match.slice(1);
+ const address = parseInt(address0 || address1, 16);
+ const sym = (symbol0 || symbol1).trimRight();
+ syms.set(address, sym);
+ approximateLength += sym.length;
+ }
+
+ return ParseSymbols.convertSymsMapToExpectedSymFormat(syms, approximateLength);
+}
+
+onmessage = async e => {
+ try {
+ let text;
+ if (e.data.filepath) {
+ text = await OS.File.read(e.data.filepath, {encoding: "utf-8"});
+ } else if (e.data.url) {
+ text = await fetchSymbolFile(e.data.url);
+ }
+
+ const result = parse(text);
+ postMessage({result}, result.map(r => r.buffer));
+ } catch (error) {
+ postMessage({error: error.toString()});
+ }
+ close();
+};
new file mode 100644
--- /dev/null
+++ b/browser/components/extensions/ParseSymbols.jsm
@@ -0,0 +1,49 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+/* exported ParseSymbols */
+
+var EXPORTED_SYMBOLS = ["ParseSymbols"];
+
+function convertStringArrayToUint8BufferWithIndex(array, approximateLength) {
+ const index = new Uint32Array(array.length + 1);
+
+ const textEncoder = new TextEncoder();
+ let buffer = new Uint8Array(approximateLength);
+ let pos = 0;
+
+ for (let i = 0; i < array.length; i++) {
+ const encodedString = textEncoder.encode(array[i]);
+
+ let size = pos + buffer.length;
+ if (size < buffer.length) {
+ size = 2 << Math.log(size) / Math.log(2);
+ let newBuffer = new Uint8Array(size);
+ newBuffer.set(buffer);
+ buffer = newBuffer;
+ }
+
+ buffer.set(encodedString, pos);
+ index[i] = pos;
+ pos += encodedString.length;
+ }
+ index[array.length] = pos;
+
+ return {index, buffer};
+}
+
+function convertSymsMapToExpectedSymFormat(syms, approximateSymLength) {
+ const addresses = Array.from(syms.keys());
+ addresses.sort((a, b) => a - b);
+
+ const symsArray = addresses.map(addr => syms.get(addr));
+ const {index, buffer} =
+ convertStringArrayToUint8BufferWithIndex(symsArray, approximateSymLength);
+
+ return [new Uint32Array(addresses), index, buffer];
+}
+
+var ParseSymbols = {
+ convertSymsMapToExpectedSymFormat,
+};
--- a/browser/components/extensions/ext-browser.js
+++ b/browser/components/extensions/ext-browser.js
@@ -175,16 +175,24 @@ extensions.registerModules({
url: "chrome://browser/content/ext-pageAction.js",
schema: "chrome://browser/content/schemas/page_action.json",
scopes: ["addon_parent"],
manifest: ["page_action"],
paths: [
["pageAction"],
],
},
+ geckoProfiler: {
+ url: "chrome://browser/content/ext-geckoProfiler.js",
+ schema: "chrome://browser/content/schemas/geckoProfiler.json",
+ scopes: ["addon_parent"],
+ paths: [
+ ["geckoProfiler"],
+ ],
+ },
sessions: {
url: "chrome://browser/content/ext-sessions.js",
schema: "chrome://browser/content/schemas/sessions.json",
scopes: ["addon_parent"],
paths: [
["sessions"],
],
},
new file mode 100644
--- /dev/null
+++ b/browser/components/extensions/ext-geckoProfiler.js
@@ -0,0 +1,363 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.importGlobalProperties(["fetch", "TextEncoder"]);
+
+XPCOMUtils.defineLazyModuleGetter(this, "OS", "resource://gre/modules/osfile.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "ParseSymbols", "resource:///modules/ParseSymbols.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Subprocess", "resource://gre/modules/Subprocess.jsm");
+
+const PREF_ASYNC_STACK = "javascript.options.asyncstack";
+const PREF_SYMBOLS_URL = "extensions.geckoProfiler.symbols.url";
+const PREF_GET_SYMBOL_RULES = "extensions.geckoProfiler.getSymbolRules";
+
+const ASYNC_STACKS_ENABLED = Services.prefs.getBoolPref(PREF_ASYNC_STACK, false);
+
+function parseSym(data) {
+ const worker = new ChromeWorker("resource://app/modules/ParseSymbols-worker.js");
+ const promise = new Promise((resolve, reject) => {
+ worker.onmessage = (e) => {
+ if (e.data.error) {
+ reject(e.data.error);
+ } else {
+ resolve(e.data.result);
+ }
+ };
+ });
+ worker.postMessage(data);
+ return promise;
+}
+
+class NMParser {
+ constructor() {
+ this._addrToSymMap = new Map();
+ this._approximateLength = 0;
+ }
+
+ consume(data) {
+ const lineRegex = /.*\n?/g;
+ const buffer = this._currentLine + data;
+
+ let match;
+ while ((match = lineRegex.exec(buffer))) {
+ let [line] = match;
+ if (line[line.length - 1] === "\n") {
+ this._processLine(line);
+ } else {
+ this._currentLine = line;
+ break;
+ }
+ }
+ }
+
+ finish() {
+ this._processLine(this._currentLine);
+ return {syms: this._addrToSymMap, approximateLength: this._approximateLength};
+ }
+
+ _processLine(line) {
+ // Example lines:
+ // 00000000028c9888 t GrFragmentProcessor::MulOutputByInputUnpremulColor(sk_sp<GrFragmentProcessor>)::PremulFragmentProcessor::onCreateGLSLInstance() const::GLFP::~GLFP()
+ // 00000000028c9874 t GrFragmentProcessor::MulOutputByInputUnpremulColor(sk_sp<GrFragmentProcessor>)::PremulFragmentProcessor::onCreateGLSLInstance() const::GLFP::~GLFP()
+ // 00000000028c9874 t GrFragmentProcessor::MulOutputByInputUnpremulColor(sk_sp<GrFragmentProcessor>)::PremulFragmentProcessor::onCreateGLSLInstance() const::GLFP::~GLFP()
+ // 0000000003a33730 r mozilla::OggDemuxer::~OggDemuxer()::{lambda()#1}::operator()() const::__func__
+ // 0000000003a33930 r mozilla::VPXDecoder::Drain()::{lambda()#1}::operator()() const::__func__
+ //
+ // Some lines have the form
+ // <address> ' ' <letter> ' ' <symbol>
+ // and some have the form
+ // <address> ' ' <symbol>
+ // The letter has a meaning, but we ignore it.
+
+ const regex = /([^ ]+) (?:. )?(.*)/;
+ let match = regex.exec(line);
+ if (match) {
+ const [, address, symbol] = match;
+ this._addrToSymMap.set(parseInt(address, 16), symbol);
+ this._approximateLength += symbol.length;
+ }
+ }
+}
+
+class CppFiltParser {
+ constructor(length) {
+ this._index = 0;
+ this._results = new Array(length);
+ }
+
+ consume(data) {
+ const lineRegex = /.*\n?/g;
+ const buffer = this._currentLine + data;
+
+ let match;
+ while ((match = lineRegex.exec(buffer))) {
+ let [line] = match;
+ if (line[line.length - 1] === "\n") {
+ this._processLine(line);
+ } else {
+ this._currentLine = line;
+ break;
+ }
+ }
+ }
+
+ finish() {
+ this._processLine(this._currentLine);
+ return this._results;
+ }
+
+ _processLine(line) {
+ this._results[this._index++] = line.trimRight();
+ }
+}
+
+async function readAllData(pipe, processData) {
+ let data;
+ while ((data = await pipe.readString())) {
+ processData(data);
+ }
+}
+
+async function spawnProcess(name, cmdArgs, processData, stdin = null) {
+ const opts = {
+ command: await Subprocess.pathSearch(name),
+ arguments: cmdArgs,
+ };
+ const proc = await Subprocess.call(opts);
+
+ if (stdin) {
+ const encoder = new TextEncoder("utf-8");
+ proc.stdin.write(encoder.encode(stdin));
+ proc.stdin.close();
+ }
+
+ await readAllData(proc.stdout, processData);
+}
+
+async function getSymbolsFromNM(path) {
+ const parser = new NMParser();
+
+ const args = [path];
+ if (Services.appinfo.OS !== "Darwin") {
+ // Mac's `nm` doesn't support the demangle option, so we have to
+ // post-process the symbols with c++filt.
+ args.unshift("--demangle");
+ }
+
+ await spawnProcess("nm", args, data => parser.consume(data));
+ await spawnProcess("nm", ["-D", ...args], data => parser.consume(data));
+ let {syms, approximateLength} = parser.finish();
+
+ if (Services.appinfo.OS === "Darwin") {
+ const keys = Array.from(syms.keys());
+ const values = keys.map(k => syms.get(k));
+ const demangler = new CppFiltParser(keys.length);
+ await spawnProcess("c++filt", [], data => demangler.consume(data), values.join("\n"));
+ const newSymbols = demangler.finish();
+ approximateLength = 0;
+ for (let [i, symbol] of newSymbols.entries()) {
+ approximateLength += symbol.length;
+ syms.set(keys[i], symbol);
+ }
+ }
+
+ return ParseSymbols.convertSymsMapToExpectedSymFormat(syms, approximateLength);
+}
+
+function pathComponentsForSymbolFile(debugName, breakpadId) {
+ const symName = debugName.replace(/(\.pdb)?$/, ".sym");
+ return [debugName, breakpadId, symName];
+}
+
+function urlForSymFile(debugName, breakpadId) {
+ const profilerSymbolsURL = Services.prefs.getCharPref(PREF_SYMBOLS_URL,
+ "http://symbols.mozilla.org/");
+ return profilerSymbolsURL + pathComponentsForSymbolFile(debugName, breakpadId).join("/");
+}
+
+function getContainingObjdirDist(path) {
+ let curPath = path;
+ let curPathBasename = OS.Path.basename(curPath);
+ while (curPathBasename) {
+ if (curPathBasename === "dist") {
+ return curPath;
+ }
+ const parentDirPath = OS.Path.dirname(curPath);
+ if (curPathBasename === "bin") {
+ return parentDirPath;
+ }
+ curPath = parentDirPath;
+ curPathBasename = OS.Path.basename(curPath);
+ }
+ return null;
+}
+
+function filePathForSymFileInObjDir(binaryPath, debugName, breakpadId) {
+ // `mach buildsymbols` generates symbol files located
+ // at /path/to/objdir/dist/crashreporter-symbols/.
+ const objDirDist = getContainingObjdirDist(binaryPath);
+ if (!objDirDist) {
+ return null;
+ }
+ return OS.Path.join(objDirDist,
+ "crashreporter-symbols",
+ ...pathComponentsForSymbolFile(debugName, breakpadId));
+}
+
+const symbolCache = new Map();
+
+function primeSymbolStore(libs) {
+ for (const {debugName, breakpadId, path} of libs) {
+ symbolCache.set(urlForSymFile(debugName, breakpadId), path);
+ }
+}
+
+const isRunningObserver = {
+ _observers: new Set(),
+
+ observe(subject, topic, data) {
+ switch (topic) {
+ case "profiler-started":
+ case "profiler-stopped":
+ // Call observer(false) or observer(true), but do it through a promise
+ // so that it's asynchronous.
+ // We don't want it to be synchronous because of the observer call in
+ // addObserver, which is asynchronous, and we want to get the ordering
+ // right.
+ const isRunningPromise = Promise.resolve(topic === "profiler-started");
+ for (let observer of this._observers) {
+ isRunningPromise.then(observer);
+ }
+ break;
+ }
+ },
+
+ _startListening() {
+ Services.obs.addObserver(this, "profiler-started");
+ Services.obs.addObserver(this, "profiler-stopped");
+ },
+
+ _stopListening() {
+ Services.obs.removeObserver(this, "profiler-started");
+ Services.obs.removeObserver(this, "profiler-stopped");
+ },
+
+ addObserver(observer) {
+ if (this._observers.size === 0) {
+ this._startListening();
+ }
+
+ this._observers.add(observer);
+ observer(Services.profiler.IsActive());
+ },
+
+ removeObserver(observer) {
+ if (this._observers.delete(observer) && this._observers.size === 0) {
+ this._stopListening();
+ }
+ },
+};
+
+this.geckoProfiler = class extends ExtensionAPI {
+ getAPI(context) {
+ return {
+ geckoProfiler: {
+ async start(options) {
+ const {bufferSize, interval, features, threads} = options;
+
+ Services.prefs.setBoolPref(PREF_ASYNC_STACK, false);
+ if (threads) {
+ Services.profiler.StartProfiler(bufferSize, interval, features, features.length, threads, threads.length);
+ } else {
+ Services.profiler.StartProfiler(bufferSize, interval, features, features.length);
+ }
+ },
+
+ async stop() {
+ if (ASYNC_STACKS_ENABLED !== null) {
+ Services.prefs.setBoolPref(PREF_ASYNC_STACK, ASYNC_STACKS_ENABLED);
+ }
+
+ Services.profiler.StopProfiler();
+ },
+
+ async pause() {
+ Services.profiler.PauseSampling();
+ },
+
+ async resume() {
+ Services.profiler.ResumeSampling();
+ },
+
+ async getProfile() {
+ if (!Services.profiler.IsActive()) {
+ throw new Error("The profiler is stopped. " +
+ "You need to start the profiler before you can capture a profile.");
+ }
+
+ return Services.profiler.getProfileDataAsync();
+ },
+
+ async getSymbols(debugName, breakpadId) {
+ if (symbolCache.size === 0) {
+ primeSymbolStore(Services.profiler.sharedLibraries);
+ }
+
+ const path = symbolCache.get(urlForSymFile(debugName, breakpadId));
+
+ const symbolRules = Services.prefs.getCharPref(PREF_GET_SYMBOL_RULES, "localBreakpad,remoteBreakpad");
+ const haveAbsolutePath = path && OS.Path.split(path).absolute;
+
+ // We have multiple options for obtaining symbol information for the given
+ // binary.
+ // "localBreakpad" - Use existing symbol dumps stored in the object directory of a local
+ // Firefox build, generated using `mach buildsymbols` [requires path]
+ // "remoteBreakpad" - Use symbol dumps from the Mozilla symbol server [only requires
+ // debugName + breakpadId]
+ // "nm" - Use the command line tool `nm` [linux/mac only, requires path]
+ for (const rule of symbolRules.split(",")) {
+ try {
+ switch (rule) {
+ case "localBreakpad":
+ if (haveAbsolutePath) {
+ const filepath = filePathForSymFileInObjDir(path, debugName, breakpadId);
+ if (filepath) {
+ // NOTE: here and below, "return await" is used to ensure we catch any
+ // errors in the promise. A simple return would give the error to the
+ // caller.
+ return await parseSym({filepath});
+ }
+ }
+ break;
+ case "remoteBreakpad":
+ const url = urlForSymFile(debugName, breakpadId);
+ return await parseSym({url});
+ case "nm":
+ return await getSymbolsFromNM(path);
+ }
+ } catch (e) {
+ // Each of our options can go wrong for a variety of reasons, so on failure
+ // we will try the next one.
+ // "localBreakpad" will fail if this is not a local build that's running from the object
+ // directory or if the user hasn't run `mach buildsymbols` on it.
+ // "remoteBreakpad" will fail if this is not an official mozilla build (e.g. Nightly) or a
+ // known system library.
+ // "nm" will fail if `nm` is not available.
+ }
+ }
+
+ throw new Error(`Ran out of options to get symbols from library ${debugName} ${breakpadId}.`);
+ },
+
+ onRunning: new SingletonEventManager(context, "geckoProfiler.onRunning", fire => {
+ isRunningObserver.addObserver(fire.async);
+ return () => {
+ isRunningObserver.removeObserver(fire.async);
+ };
+ }).api(),
+ },
+ };
+ }
+};
--- a/browser/components/extensions/jar.mn
+++ b/browser/components/extensions/jar.mn
@@ -18,16 +18,17 @@ browser.jar:
content/browser/ext-browsingData.js
content/browser/ext-chrome-settings-overrides.js
content/browser/ext-commands.js
content/browser/ext-contextMenus.js
content/browser/ext-devtools.js
content/browser/ext-devtools-inspectedWindow.js
content/browser/ext-devtools-network.js
content/browser/ext-devtools-panels.js
+ content/browser/ext-geckoProfiler.js
content/browser/ext-history.js
content/browser/ext-omnibox.js
content/browser/ext-pageAction.js
content/browser/ext-sessions.js
content/browser/ext-sidebarAction.js
content/browser/ext-tabs.js
content/browser/ext-url-overrides.js
content/browser/ext-utils.js
--- a/browser/components/extensions/moz.build
+++ b/browser/components/extensions/moz.build
@@ -10,16 +10,18 @@ with Files("**"):
JAR_MANIFESTS += ['jar.mn']
EXTRA_COMPONENTS += [
'extensions-browser.manifest',
]
EXTRA_JS_MODULES += [
'ExtensionPopups.jsm',
+ 'ParseSymbols-worker.js',
+ 'ParseSymbols.jsm',
]
DIRS += ['schemas']
BROWSER_CHROME_MANIFESTS += [
'test/browser/browser-remote.ini',
'test/browser/browser.ini',
]
new file mode 100644
--- /dev/null
+++ b/browser/components/extensions/schemas/geckoProfiler.json
@@ -0,0 +1,129 @@
+[
+ {
+ "namespace": "manifest",
+ "types": [
+ {
+ "$extend": "Permission",
+ "choices": [{
+ "type": "string",
+ "enum": [
+ "geckoProfiler"
+ ]
+ }]
+ }
+ ]
+ },
+ {
+ "namespace": "geckoProfiler",
+ "description": "Exposes the browser's profiler.",
+
+ "permissions": ["geckoProfiler"],
+ "functions": [
+ {
+ "name": "start",
+ "type": "function",
+ "description": "Starts the profiler with the specified settings.",
+ "async": true,
+ "parameters": [
+ {
+ "name": "settings",
+ "type": "object",
+ "properties": {
+ "bufferSize": {
+ "type": "integer",
+ "minimum": 0,
+ "description": "The size in bytes of the buffer used to store profiling data. A larger value allows capturing a profile that covers a greater amount of time."
+ },
+ "interval": {
+ "type": "number",
+ "description": "Interval in milliseconds between samples of profiling data. A smaller value will increase the detail of the profiles captured."
+ },
+ "features": {
+ "type": "array",
+ "description": "A list of active features for the profiler.",
+ "items": {
+ "type": "string",
+ "enum": [
+ "js",
+ "stackwalk",
+ "tasktracer",
+ "leaf",
+ "threads"
+ ]
+ }
+ },
+ "threads": {
+ "type": "array",
+ "description": "A list of thread names for which to capture profiles.",
+ "optional": true,
+ "items": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ ]
+ },
+ {
+ "name": "stop",
+ "type": "function",
+ "description": "Stops the profiler and discards any captured profile data.",
+ "async": true,
+ "parameters": []
+ },
+ {
+ "name": "pause",
+ "type": "function",
+ "description": "Pauses the profiler, keeping any profile data that is already written.",
+ "async": true,
+ "parameters": []
+ },
+ {
+ "name": "resume",
+ "type": "function",
+ "description": "Resumes the profiler with the settings that were initially used to start it.",
+ "async": true,
+ "parameters": []
+ },
+ {
+ "name": "getProfile",
+ "type": "function",
+ "description": "Gathers the profile data from the current profiling session.",
+ "async": true,
+ "parameters": []
+ },
+ {
+ "name": "getSymbols",
+ "type": "function",
+ "description": "Gets the debug symbols for a particular library.",
+ "async": true,
+ "parameters": [
+ {
+ "type": "string",
+ "name": "debugName",
+ "description": "The name of the library's debug file. For example, 'xul.pdb"
+ },
+ {
+ "type": "string",
+ "name": "breakpadId",
+ "description": "The Breakpad ID of the library"
+ }
+ ]
+ }
+ ],
+ "events": [
+ {
+ "name": "onRunning",
+ "type": "function",
+ "description": "Fires when the profiler starts/stops running.",
+ "parameters": [
+ {
+ "name": "isRunning",
+ "type": "boolean",
+ "description": "Whether the profiler is running or not. Pausing the profiler will not affect this value."
+ }
+ ]
+ }
+ ]
+ }
+]
--- a/browser/components/extensions/schemas/jar.mn
+++ b/browser/components/extensions/schemas/jar.mn
@@ -9,16 +9,17 @@ browser.jar:
content/browser/schemas/chrome_settings_overrides.json
content/browser/schemas/commands.json
content/browser/schemas/context_menus.json
content/browser/schemas/context_menus_internal.json
content/browser/schemas/devtools.json
content/browser/schemas/devtools_inspected_window.json
content/browser/schemas/devtools_network.json
content/browser/schemas/devtools_panels.json
+ content/browser/schemas/geckoProfiler.json
content/browser/schemas/history.json
content/browser/schemas/omnibox.json
content/browser/schemas/page_action.json
content/browser/schemas/sessions.json
content/browser/schemas/sidebar_action.json
content/browser/schemas/tabs.json
content/browser/schemas/url_overrides.json
content/browser/schemas/windows.json
--- a/browser/components/extensions/test/browser/browser-common.ini
+++ b/browser/components/extensions/test/browser/browser-common.ini
@@ -1,13 +1,14 @@
[DEFAULT]
support-files =
head.js
head_pageAction.js
head_sessions.js
+ profilerSymbols.sjs
context.html
context_frame.html
ctxmenu-image.png
context_tabs_onUpdated_page.html
context_tabs_onUpdated_iframe.html
file_clearplugindata.html
file_popup_api_injection_a.html
file_popup_api_injection_b.html
@@ -57,16 +58,17 @@ support-files =
[browser_ext_contextMenus_uninstall.js]
[browser_ext_contextMenus_urlPatterns.js]
[browser_ext_currentWindow.js]
[browser_ext_devtools_inspectedWindow.js]
[browser_ext_devtools_inspectedWindow_reload.js]
[browser_ext_devtools_network.js]
[browser_ext_devtools_page.js]
[browser_ext_devtools_panel.js]
+[browser_ext_geckoProfiler_symbolicate.js]
[browser_ext_getViews.js]
[browser_ext_incognito_views.js]
[browser_ext_incognito_popup.js]
[browser_ext_lastError.js]
[browser_ext_omnibox.js]
skip-if = debug || asan # Bug 1354681
[browser_ext_optionsPage_browser_style.js]
[browser_ext_optionsPage_privileges.js]
new file mode 100644
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_geckoProfiler_symbolicate.js
@@ -0,0 +1,57 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+let getExtension = () => {
+ return ExtensionTestUtils.loadExtension({
+ background: async function() {
+ const [addresses, indices, strings] = await browser.geckoProfiler.getSymbols("test.pdb",
+ "ASDFQWERTY");
+
+ function getSymbolAtAddress(address) {
+ const index = addresses.indexOf(address);
+ if (index == -1) {
+ return null;
+ }
+
+ const nameBuffer = strings.subarray(indices[index], indices[index + 1]);
+ const decoder = new TextDecoder("utf-8");
+
+ return decoder.decode(nameBuffer);
+ }
+
+ browser.test.assertEq(getSymbolAtAddress(0x3fc74), "test_public_symbol", "Contains public symbol");
+ browser.test.assertEq(getSymbolAtAddress(0x40330), "test_func_symbol", "Contains func symbol");
+ browser.test.sendMessage("symbolicated");
+ },
+
+ manifest: {
+ "permissions": ["geckoProfiler"],
+ "applications": {
+ "gecko": {
+ "id": "profilertest@mozilla.com",
+ },
+ },
+ },
+ });
+};
+
+add_task(function* testProfilerControl() {
+ SpecialPowers.pushPrefEnv({
+ set: [
+ [
+ "extensions.geckoProfiler.symbols.url",
+ "http://mochi.test:8888/browser/browser/components/extensions/test/browser/profilerSymbols.sjs?path=",
+ ],
+ [
+ "extensions.geckoProfiler.acceptedExtensionIds",
+ "profilertest@mozilla.com",
+ ],
+ ],
+ });
+
+ let extension = getExtension();
+ yield extension.startup();
+ yield extension.awaitMessage("symbolicated");
+ yield extension.unload();
+});
new file mode 100644
--- /dev/null
+++ b/browser/components/extensions/test/browser/profilerSymbols.sjs
@@ -0,0 +1,27 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const SYMBOLS_FILE =
+`MODULE windows x86_64 A712ED458B2542C18785C19D17D64D842 test.pdb
+
+INFO CODE_ID 58EE0F7F3EDD000 test.dll
+
+PUBLIC 3fc74 0 test_public_symbol
+
+FUNC 40330 8e 0 test_func_symbol
+
+40330 42 22 2229
+
+40372 3a 23 2229
+
+403ac 12 23 2229
+`;
+
+function handleRequest(req, resp) {
+ let match = /path=([^\/]+)\/([^\/]+)\/([^\/]+)/.exec(req.queryString);
+ if (match && match[1] == "test.pdb") {
+ resp.write(SYMBOLS_FILE);
+ } else {
+ resp.setStatusLine(null, 404, 'Not Found');
+ }
+}
new file mode 100644
--- /dev/null
+++ b/browser/components/extensions/test/xpcshell/test_ext_geckoProfiler_control.js
@@ -0,0 +1,104 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+let getExtension = () => {
+ return ExtensionTestUtils.loadExtension({
+ background: async function() {
+ const runningListener = isRunning => {
+ if (isRunning) {
+ browser.test.sendMessage("started");
+ } else {
+ browser.test.sendMessage("stopped");
+ }
+ };
+
+ browser.test.onMessage.addListener(async message => {
+ let result;
+ switch (message) {
+ case "start":
+ result = await browser.geckoProfiler.start({
+ bufferSize: 10000,
+ interval: 0.5,
+ features: ["js"],
+ threads: ["GeckoMain"],
+ });
+ browser.test.assertEq(undefined, result, "start returns nothing.");
+ break;
+ case "stop":
+ result = await browser.geckoProfiler.stop();
+ browser.test.assertEq(undefined, result, "stop returns nothing.");
+ break;
+ case "pause":
+ result = await browser.geckoProfiler.pause();
+ browser.test.assertEq(undefined, result, "pause returns nothing.");
+ browser.test.sendMessage("paused");
+ break;
+ case "resume":
+ result = await browser.geckoProfiler.resume();
+ browser.test.assertEq(undefined, result, "resume returns nothing.");
+ browser.test.sendMessage("resumed");
+ break;
+ case "test profile":
+ result = await browser.geckoProfiler.getProfile();
+ browser.test.assertTrue("libs" in result, "The profile contains libs.");
+ browser.test.assertTrue("meta" in result, "The profile contains meta.");
+ browser.test.assertTrue("threads" in result, "The profile contains threads.");
+ browser.test.assertTrue(result.threads.some(t => t.name == "GeckoMain"),
+ "The profile contains a GeckoMain thread.");
+ browser.test.sendMessage("tested profile");
+ break;
+ case "remove runningListener":
+ browser.geckoProfiler.onRunning.removeListener(runningListener);
+ browser.test.sendMessage("removed runningListener");
+ break;
+ }
+ });
+
+ browser.test.sendMessage("ready");
+
+ browser.geckoProfiler.onRunning.addListener(runningListener);
+ },
+
+ manifest: {
+ "permissions": ["geckoProfiler"],
+ "applications": {
+ "gecko": {
+ "id": "profilertest@mozilla.com",
+ },
+ },
+ },
+ });
+};
+
+add_task(async function testProfilerControl() {
+ const acceptedExtensionIdsPref = "extensions.geckoProfiler.acceptedExtensionIds";
+ Services.prefs.setCharPref(acceptedExtensionIdsPref, "profilertest@mozilla.com");
+
+ let extension = getExtension();
+ await extension.startup();
+ await extension.awaitMessage("ready");
+ await extension.awaitMessage("stopped");
+
+ extension.sendMessage("start");
+ await extension.awaitMessage("started");
+
+ extension.sendMessage("test profile");
+ await extension.awaitMessage("tested profile");
+
+ extension.sendMessage("pause");
+ await extension.awaitMessage("paused");
+
+ extension.sendMessage("resume");
+ await extension.awaitMessage("resumed");
+
+ extension.sendMessage("stop");
+ await extension.awaitMessage("stopped");
+
+ extension.sendMessage("remove runningListener");
+ await extension.awaitMessage("removed runningListener");
+
+ await extension.unload();
+
+ Services.prefs.clearUserPref(acceptedExtensionIdsPref);
+});
--- a/browser/components/extensions/test/xpcshell/xpcshell.ini
+++ b/browser/components/extensions/test/xpcshell/xpcshell.ini
@@ -8,8 +8,9 @@ tags = webextensions
[test_ext_browsingData_cookies_cache.js]
[test_ext_browsingData_downloads.js]
[test_ext_browsingData_passwords.js]
[test_ext_browsingData_settings.js]
[test_ext_history.js]
[test_ext_manifest_commands.js]
[test_ext_manifest_omnibox.js]
[test_ext_manifest_permissions.js]
+[test_ext_geckoProfiler_control.js]
--- a/toolkit/components/extensions/Extension.jsm
+++ b/toolkit/components/extensions/Extension.jsm
@@ -514,16 +514,24 @@ this.ExtensionData = class {
}
let whitelist = [];
for (let perm of this.manifest.permissions) {
if (perm == "contextualIdentities" && !Preferences.get("privacy.userContext.enabled")) {
continue;
}
+ if (perm === "geckoProfiler") {
+ const acceptedExtensions = Preferences.get("extensions.geckoProfiler.acceptedExtensionIds");
+ if (!acceptedExtensions.split(",").includes(this.id)) {
+ this.manifestError("Only whitelisted extensions are allowed to access the geckoProfiler.");
+ continue;
+ }
+ }
+
this.permissions.add(perm);
let type = classifyPermission(perm);
if (type.origin) {
whitelist.push(perm);
} else if (type.api) {
this.apiNames.add(type.api);
}
}
--- a/toolkit/components/extensions/ExtensionCommon.jsm
+++ b/toolkit/components/extensions/ExtensionCommon.jsm
@@ -1061,17 +1061,17 @@ class SchemaAPIManager extends EventEmit
* @returns {object} A sandbox that is used as the global by `loadScript`.
*/
_createExtGlobal() {
let global = Cu.Sandbox(Services.scriptSecurityManager.getSystemPrincipal(), {
wantXrays: false,
sandboxName: `Namespace of ext-*.js scripts for ${this.processType}`,
});
- Object.assign(global, {global, Cc, Ci, Cu, Cr, XPCOMUtils, extensions: this});
+ Object.assign(global, {global, Cc, Ci, Cu, Cr, XPCOMUtils, ChromeWorker, extensions: this});
Cu.import("resource://gre/modules/AppConstants.jsm", global);
Cu.import("resource://gre/modules/ExtensionAPI.jsm", global);
XPCOMUtils.defineLazyGetter(global, "console", getConsole);
XPCOMUtils.defineLazyModuleGetter(global, "ExtensionUtils",
"resource://gre/modules/ExtensionUtils.jsm");