Bug 1326572 - Provide an API for nsIProfiler draft
authorDoug Thayer <dothayer@mozilla.com>
Tue, 11 Apr 2017 09:03:40 -0700
changeset 569077 0ef2da5959f4586db9a5cd0c31cf0b24bf387d51
parent 568807 a477e80f03b61be9961bc61770a2b55cce139b91
child 626104 7e8d1d5bba504761dcfdc8ecfc38edd58d23b7aa
push id56062
push userbmo:dothayer@mozilla.com
push dateWed, 26 Apr 2017 23:14:22 +0000
bugs1326572
milestone55.0a1
Bug 1326572 - Provide an API for nsIProfiler In order to rewrite the Gecko Profiler add-on as a WebExtension, we need an API for the profiler which allows us to control the nsIProfiler, and symbolicate the stacks that it provides. This is the implementation of the simpler parts of that API. TODO: - Support profiling of remote targets through a new devtools API. - Support the dump_syms breakpad code which was asm.js in the old extension by directly calling into native code. - Figure out a faster way to send the large volume of data from getSymbols all the way from our extension down to the content process and then into the page's context. MozReview-Commit-ID: JzDbV4l2eXd
browser/app/profile/firefox.js
browser/components/extensions/ParseSymbols-worker.js
browser/components/extensions/ParseSymbols.jsm
browser/components/extensions/ext-browser.js
browser/components/extensions/ext-geckoProfiler.js
browser/components/extensions/jar.mn
browser/components/extensions/moz.build
browser/components/extensions/schemas/geckoProfiler.json
browser/components/extensions/schemas/jar.mn
browser/components/extensions/test/browser/browser-common.ini
browser/components/extensions/test/browser/browser_ext_geckoProfiler_symbolicate.js
browser/components/extensions/test/browser/profilerSymbols.sjs
browser/components/extensions/test/xpcshell/test_ext_geckoProfiler_control.js
browser/components/extensions/test/xpcshell/xpcshell.ini
toolkit/components/extensions/Extension.jsm
toolkit/components/extensions/ExtensionCommon.jsm
--- 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");