bug 1296898 - Memory api (pulled from erahm's experiment) as a part of the webextension api r=mixedpuppy
authorKristen Wright <kwright@mozilla.com>
Wed, 11 Sep 2019 17:34:04 +0000
changeset 492705 8e400446aedc740c18aceab2ca6d59fa723bcea5
parent 492704 fc52b9f9872fb9e8224a970090c2e29bbd617a68
child 492706 fc57ffc08856796b1bbb47129b210dedf33a6c1f
push id36563
push usercbrindusan@mozilla.com
push dateWed, 11 Sep 2019 21:53:06 +0000
treeherdermozilla-central@26711f10f5f4 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmixedpuppy
bugs1296898
milestone71.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
bug 1296898 - Memory api (pulled from erahm's experiment) as a part of the webextension api r=mixedpuppy Webextension api to get memory usage. Also brings back memory.jsm, as it is used by the api. Adds test_ext_memory.js to test ext-memory.js' functionality and permissions. Differential Revision: https://phabricator.services.mozilla.com/D38536
toolkit/components/extensions/Extension.jsm
toolkit/components/extensions/ext-toolkit.json
toolkit/components/extensions/jar.mn
toolkit/components/extensions/parent/ext-memory.js
toolkit/components/extensions/schemas/jar.mn
toolkit/components/extensions/schemas/memory.json
toolkit/components/extensions/test/xpcshell/test_ext_memory.js
toolkit/components/extensions/test/xpcshell/test_ext_permissions.js
toolkit/components/extensions/test/xpcshell/xpcshell.ini
toolkit/modules/Memory.jsm
toolkit/modules/moz.build
--- a/toolkit/components/extensions/Extension.jsm
+++ b/toolkit/components/extensions/Extension.jsm
@@ -149,16 +149,17 @@ const CHILD_SHUTDOWN_TIMEOUT_MS = 8000;
 const PRIVILEGED_PERMS = new Set([
   "activityLog",
   "mozillaAddons",
   "geckoViewAddons",
   "telemetry",
   "urlbar",
   "normandyAddonStudy",
   "networkStatus",
+  "memory",
 ]);
 
 /**
  * Classify an individual permission from a webextension manifest
  * as a host/origin permission, an api permission, or a regular permission.
  *
  * @param {string} perm  The permission string to classify
  * @param {boolean} restrictSchemes
--- a/toolkit/components/extensions/ext-toolkit.json
+++ b/toolkit/components/extensions/ext-toolkit.json
@@ -109,16 +109,24 @@
   "management": {
     "url": "chrome://extensions/content/parent/ext-management.js",
     "schema": "chrome://extensions/content/schemas/management.json",
     "scopes": ["addon_parent"],
     "paths": [
       ["management"]
     ]
   },
+  "memory" : {
+    "url": "chrome://extensions/content/parent/ext-memory.js",
+    "schema": "chrome://extensions/content/schemas/memory.json",
+    "scopes": ["addon_parent"],
+    "paths": [
+      ["memory"]
+    ]
+  },
   "networkStatus": {
     "url": "chrome://extensions/content/parent/ext-networkStatus.js",
     "schema": "chrome://extensions/content/schemas/network_status.json",
     "scopes": ["addon_parent"],
     "paths": [
       ["networkStatus"]
     ]
   },
--- a/toolkit/components/extensions/jar.mn
+++ b/toolkit/components/extensions/jar.mn
@@ -25,16 +25,17 @@ toolkit.jar:
     content/extensions/parent/ext-geckoProfiler.js (parent/ext-geckoProfiler.js)
 #endif
     content/extensions/parent/ext-i18n.js (parent/ext-i18n.js)
 #ifndef ANDROID
     content/extensions/parent/ext-identity.js (parent/ext-identity.js)
 #endif
     content/extensions/parent/ext-idle.js (parent/ext-idle.js)
     content/extensions/parent/ext-management.js (parent/ext-management.js)
+    content/extensions/parent/ext-memory.js (parent/ext-memory.js)
     content/extensions/parent/ext-networkStatus.js (parent/ext-networkStatus.js)
     content/extensions/parent/ext-notifications.js (parent/ext-notifications.js)
     content/extensions/parent/ext-permissions.js (parent/ext-permissions.js)
     content/extensions/parent/ext-privacy.js (parent/ext-privacy.js)
     content/extensions/parent/ext-protocolHandlers.js (parent/ext-protocolHandlers.js)
     content/extensions/parent/ext-proxy.js (parent/ext-proxy.js)
     content/extensions/parent/ext-runtime.js (parent/ext-runtime.js)
     content/extensions/parent/ext-storage.js (parent/ext-storage.js)
new file mode 100644
--- /dev/null
+++ b/toolkit/components/extensions/parent/ext-memory.js
@@ -0,0 +1,134 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 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 https://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+ChromeUtils.defineModuleGetter(
+  this,
+  "Memory",
+  "resource://gre/modules/Memory.jsm"
+);
+ChromeUtils.defineModuleGetter(
+  this,
+  "Services",
+  "resource://gre/modules/Services.jsm"
+);
+
+// Helper class that handles observing a topic and forwarding the data payload
+// to a set of registered listeners.
+class ListenerRelay {
+  /**
+   * @param {string} topic   The topic that will be observed.
+   **/
+  constructor(topic) {
+    this.listeners = new Set();
+    this.topic = topic;
+  }
+
+  // Removes ourselves from the observer service
+  // and clears out all registered listeners.
+  clear() {
+    if (this.listeners.size) {
+      Services.obs.removeObserver(this, this.topic);
+      this.listeners.clear();
+    }
+  }
+
+  /**
+   * Adds a callback to invoke when our topic is observed.
+   * @param {function} callback   The callback to invoke.
+   **/
+  add(callback) {
+    if (!this.listeners.size) {
+      Services.obs.addObserver(this, this.topic);
+    }
+
+    this.listeners.add(callback);
+  }
+
+  /**
+   * Removes a callback that was previously registered.
+   * @param {function} callback   The callback to remove.
+   **/
+  remove(callback) {
+    this.listeners.delete(callback);
+
+    if (!this.listeners.size) {
+      Services.obs.removeObserver(this, this.topic);
+    }
+  }
+
+  /**
+   * Callback function invoked by the observer service when our topic is hit.
+   * @param {null}   subject  Not being used by this function
+   * @param {string} topic    The topic being observed. This should only be
+   *                          `this.topic`.
+   * @param {string} data     The data payload that will be forwarded to the
+   *                          listeners.
+   **/
+  observe(subject, topic, data) {
+    if (topic !== this.topic) {
+      return;
+    }
+
+    // Forward to our set of listeners.
+    for (let listener of this.listeners) {
+      try {
+        listener(data);
+      } catch (e) {
+        Cu.reportError(e);
+      }
+    }
+  }
+}
+
+// Relay used for memory-pressure events.
+XPCOMUtils.defineLazyGetter(this, "lowMemListeners", () => {
+  return new ListenerRelay("memory-pressure");
+});
+
+// The browser.memory API.
+this.memory = class extends ExtensionAPI {
+  getAPI(context) {
+    return {
+      memory: {
+        // Super basic, this gives back USS and RSS of each process.
+        // Other interesting things to add:
+        //   - Ghost windows
+        //   - Available memory (chrome does this)
+        //   - Capacity (I guess physical memory, chrome does this)
+        //   - VSS
+        async getInfo() {
+          return Memory.summary();
+        },
+
+        // Provides the same functionality as about:memory's "minimize memory
+        // usage" button.
+        async minimizeMemoryUsage() {
+          await new Promise(resolve => {
+            const mgr = Cc["@mozilla.org/memory-reporter-manager;1"].getService(
+              Ci.nsIMemoryReporterManager
+            );
+            mgr.minimizeMemoryUsage(resolve);
+          });
+        },
+
+        // Event that is fired when a low memory signal is recieved.
+        onLowMemory: new ExtensionCommon.EventManager({
+          context,
+          name: "memory.onLowMemory",
+          register: fire => {
+            // Register the listener with our relay.
+            lowMemListeners.add(fire.async);
+            return () => {
+              lowMemListeners.remove(fire.async);
+            };
+          },
+        }).api(),
+      },
+    };
+  }
+};
--- a/toolkit/components/extensions/schemas/jar.mn
+++ b/toolkit/components/extensions/schemas/jar.mn
@@ -26,16 +26,17 @@ toolkit.jar:
 #endif
     content/extensions/schemas/i18n.json
 #ifndef ANDROID
     content/extensions/schemas/identity.json
 #endif
     content/extensions/schemas/idle.json
     content/extensions/schemas/management.json
     content/extensions/schemas/manifest.json
+    content/extensions/schemas/memory.json
     content/extensions/schemas/native_manifest.json
     content/extensions/schemas/network_status.json
     content/extensions/schemas/notifications.json
     content/extensions/schemas/permissions.json
     content/extensions/schemas/proxy.json
     content/extensions/schemas/privacy.json
     content/extensions/schemas/runtime.json
     content/extensions/schemas/storage.json
new file mode 100644
--- /dev/null
+++ b/toolkit/components/extensions/schemas/memory.json
@@ -0,0 +1,56 @@
+[
+  {
+    "namespace": "manifest",
+    "types": [
+      {
+        "$extend": "Permission",
+        "choices": [{
+          "type": "string",
+          "enum": [
+            "memory"
+          ]
+        }]
+      }
+    ]
+  },
+  {
+    "namespace": "memory",
+    "description": "API for querying browser memory statistics",
+    "permissions": ["memory"],
+    "functions": [
+      {
+        "name": "getInfo",
+        "type": "function",
+        "description": "Returns an object containing memory info",
+        "async": true,
+        "parameters": []
+      },
+      {
+        "name": "minimizeMemoryUsage",
+        "type": "function",
+        "description": "Minimizes memory used by the browser if possible.",
+        "async": true,
+        "parameters": []
+      }
+    ],
+    "events": [
+      {
+        "name": "onLowMemory",
+        "type": "function",
+        "description": "Event fired when memory is low.",
+        "parameters": [
+          {
+            "name": "reason",
+            "type": "string",
+            "enum": [
+              "low-memory",
+              "low-memory-ongoing",
+              "heap-minimize"
+            ],
+            "description": "Memory pressure notifications. Low-memory is passed when the pressure observer is being asked to flush, and low-memory-ongoing is passed when a low-memory condition persists. Heap-minimize is passed specifically for heap minimize calls."
+          }
+        ]
+      }
+    ]
+  }
+]
new file mode 100644
--- /dev/null
+++ b/toolkit/components/extensions/test/xpcshell/test_ext_memory.js
@@ -0,0 +1,119 @@
+"use strict";
+
+AddonTestUtils.init(this);
+
+add_task(async function test_memory_getInfo() {
+  // Background script used for the test extension.
+  //
+  // Adds a listener that proxies commands, for instance you can send:
+  //    'getInfo.request'
+  // and that will run the `memory.getInfo` command and responde with a
+  //    'getInfo.done'
+  // message including an error message or the results.
+  //
+  // Broadcasts a 'ready' message when loaded.
+  function background() {
+    const onLowMem = data => {
+      browser.test.sendMessage("onLowMemory", { data });
+    };
+
+    browser.test.onMessage.addListener(async (msg, args) => {
+      let match = msg.match(/^(\w+)\.request$/);
+      if (!match) {
+        return;
+      }
+      let cmd = match[1];
+
+      if (cmd === "registerLowMem") {
+        browser.memory.onLowMemory.addListener(onLowMem);
+        browser.test.sendMessage(`${cmd}.done`, {});
+        return;
+      } else if (cmd === "unregisterLowMem") {
+        browser.memory.onLowMemory.removeListener(onLowMem);
+        browser.test.sendMessage(`${cmd}.done`, {});
+        return;
+      }
+
+      try {
+        let results = await browser.memory[cmd](...args);
+        browser.test.sendMessage(`${cmd}.done`, { results });
+      } catch (e) {
+        browser.test.sendMessage(`${cmd}.done`, { errmsg: e.message });
+      }
+    });
+  }
+
+  function run(ext, cmd, ...args) {
+    let promise = ext.awaitMessage(`${cmd}.done`);
+    ext.sendMessage(`${cmd}.request`, args);
+    return promise;
+  }
+
+  // Create a temporary extension that uses the memory api.
+  let privilegedExtension = ExtensionTestUtils.loadExtension({
+    background,
+    manifest: {
+      permissions: ["memory"],
+    },
+    isPrivileged: true,
+  });
+
+  await privilegedExtension.startup();
+
+  // Now we can actually invoke the extension api.
+  let response = await run(privilegedExtension, "getInfo");
+
+  // We expect to get back an object similar to:
+  // { "results": { "Parent": { "uss": 123, "rss": 123 } } }
+  // Since this is just a test we don't expect child processes.
+  let results = response.results;
+  ok(
+    results.hasOwnProperty("Parent"),
+    "Should have memory object for the parent process."
+  );
+  ok(results.Parent.uss > 0, "uss value must be nonzero.");
+  ok(results.Parent.rss > 0, "rss value must be nonzero.");
+
+  response = await run(privilegedExtension, "registerLowMem");
+  equal(
+    Object.keys(response).length,
+    0,
+    "Low memory listeners should register."
+  );
+
+  // Trigger a low memory event
+  let p = privilegedExtension.awaitMessage("onLowMemory");
+  Services.obs.notifyObservers(null, "memory-pressure", "heap-minimize");
+  response = await p;
+  ok(response.hasOwnProperty("data"), "Response data should exist.");
+  equal(
+    response.data,
+    "heap-minimize",
+    "Heap-minimize low memory event should trigger."
+  );
+
+  response = await run(privilegedExtension, "unregisterLowMem");
+  equal(
+    Object.keys(response).length,
+    0,
+    "Low memory listeners should unregister."
+  );
+
+  // Try minimizing memory.
+  response = await run(privilegedExtension, "minimizeMemoryUsage");
+
+  await privilegedExtension.unload();
+});
+
+add_task(async function test_memory_permission() {
+  let extension = ExtensionTestUtils.loadExtension({
+    manifest: {
+      permissions: ["memory"],
+    },
+    background() {
+      browser.test.assertEq(browser.memory, undefined, "memory is privileged");
+    },
+  });
+  await extension.startup();
+  await extension.unload();
+});
--- a/toolkit/components/extensions/test/xpcshell/test_ext_permissions.js
+++ b/toolkit/components/extensions/test/xpcshell/test_ext_permissions.js
@@ -565,16 +565,17 @@ const GRANTED_WITHOUT_USER_PROMPT = [
   "captivePortal",
   "contextMenus",
   "contextualIdentities",
   "cookies",
   "activityLog",
   "geckoProfiler",
   "identity",
   "idle",
+  "memory",
   "menus",
   "menus.overrideContext",
   "mozillaAddons",
   "networkStatus",
   "normandyAddonStudy",
   "search",
   "storage",
   "telemetry",
--- a/toolkit/components/extensions/test/xpcshell/xpcshell.ini
+++ b/toolkit/components/extensions/test/xpcshell/xpcshell.ini
@@ -37,16 +37,17 @@ tags = webextensions in-process-webexten
 skip-if = os == 'android' # Not shipped on Android
 [test_ext_manifest.js]
 skip-if = toolkit == 'android' # browser_action icon testing not supported on android
 [test_ext_manifest_content_security_policy.js]
 [test_ext_manifest_incognito.js]
 [test_ext_manifest_minimum_chrome_version.js]
 [test_ext_manifest_minimum_opera_version.js]
 [test_ext_manifest_themes.js]
+[test_ext_memory.js]
 [test_ext_permission_warnings.js]
 [test_ext_schemas.js]
 [test_ext_schemas_roots.js]
 [test_ext_schemas_async.js]
 [test_ext_schemas_allowed_contexts.js]
 [test_ext_schemas_interactive.js]
 skip-if = os == 'android' && processor == 'x86_64'
 [test_ext_schemas_manifest_permissions.js]
new file mode 100644
--- /dev/null
+++ b/toolkit/modules/Memory.jsm
@@ -0,0 +1,95 @@
+/* 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/. */
+
+var EXPORTED_SYMBOLS = ["Memory"];
+
+// How long we should wait for the Promise to resolve.
+const TIMEOUT_INTERVAL = 2000;
+
+const { XPCOMUtils } = ChromeUtils.import(
+  "resource://gre/modules/XPCOMUtils.jsm"
+);
+const { setTimeout, clearTimeout } = ChromeUtils.import(
+  "resource://gre/modules/Timer.jsm"
+);
+const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
+
+XPCOMUtils.defineLazyServiceGetter(
+  this,
+  "memory",
+  "@mozilla.org/memory-reporter-manager;1",
+  "nsIMemoryReporterManager"
+);
+
+var Memory = {
+  /**
+   * This function returns a Promise that resolves with an Object that
+   * describes basic memory usage for each content process and the parent
+   * process.
+   * @returns Promise
+   * @resolves JS Object
+   * An Object in the following format:
+   * {
+   *   "parent": {
+   *     uss: <int>,
+   *     rss: <int>,
+   *     ghosts: <int>,
+   *   },
+   *   <pid>: {
+   *     uss: <int>,
+   *     rss: <int>,
+   *     ghosts: <int>,
+   *   },
+   *   ...
+   * }
+   */
+  summary() {
+    if (!this._pendingPromise) {
+      this._pendingPromise = new Promise(resolve => {
+        this._pendingResolve = resolve;
+        this._summaries = {};
+        this._origChildCount = Services.ppmm.childCount;
+        Services.ppmm.broadcastAsyncMessage("Memory:GetSummary");
+        Services.ppmm.addMessageListener("Memory:Summary", this);
+        this._pendingTimeout = setTimeout(() => {
+          this.finish();
+        }, TIMEOUT_INTERVAL);
+      });
+    }
+    return this._pendingPromise;
+  },
+
+  receiveMessage(msg) {
+    if (msg.name != "Memory:Summary" || !this._pendingResolve) {
+      return;
+    }
+    this._summaries[msg.data.pid] = msg.data.summary;
+    // Now we check if we are done for all content processes.
+    // Services.ppmm.childCount is a count of how many processes currently
+    // exist that might respond to messages sent through the ppmm, including
+    // the parent process. So we subtract the parent process with the "- 1",
+    // and that’s how many content processes we’re waiting for.
+    if (
+      Object.keys(this._summaries).length >=
+      Math.min(this._origChildCount, Services.ppmm.childCount) - 1
+    ) {
+      this.finish();
+    }
+  },
+
+  finish() {
+    // Code to gather the USS and RSS values for the parent process. This
+    // functions the same way as in process-content.js.
+    let rss = memory.resident;
+    let uss = memory.residentUnique;
+    let ghosts = memory.ghostWindows;
+    this._summaries.Parent = { uss, rss, ghosts };
+    this._pendingResolve(this._summaries);
+    this._pendingResolve = null;
+    this._summaries = null;
+    this._pendingPromise = null;
+    clearTimeout(this._pendingTimeout);
+    Services.ppmm.removeMessageListener("Memory:Summary", this);
+  },
+};
--- a/toolkit/modules/moz.build
+++ b/toolkit/modules/moz.build
@@ -85,16 +85,19 @@ with Files('Integration.jsm'):
     BUG_COMPONENT = ('Toolkit', 'Async Tooling')
 
 with Files('JSONFile.jsm'):
     BUG_COMPONENT = ('Toolkit', 'Form Manager')
 
 with Files('LightweightThemeConsumer.jsm'):
     BUG_COMPONENT = ('Firefox', 'Toolbars and Customization')
 
+with Files('Memory.jsm'):
+    BUG_COMPONENT = ('Core', 'DOM: Content Processes')
+
 with Files('NLP.jsm'):
     BUG_COMPONENT = ('Toolkit', 'Find Toolbar')
 
 with Files('NewTabUtils.jsm'):
     BUG_COMPONENT = ('Firefox', 'Tabbed Browser')
 
 with Files('ObjectUtils.jsm'):
     BUG_COMPONENT = ('Toolkit', 'Telemetry')
@@ -198,16 +201,17 @@ EXTRA_JS_MODULES += [
     'Http.jsm',
     'IgnoreLists.jsm',
     'IndexedDB.jsm',
     'InlineSpellChecker.jsm',
     'InlineSpellCheckerContent.jsm',
     'Integration.jsm',
     'JSONFile.jsm',
     'Log.jsm',
+    'Memory.jsm',
     'NewTabUtils.jsm',
     'NLP.jsm',
     'ObjectUtils.jsm',
     'offlineAppCache.jsm',
     'PageMenu.jsm',
     'PermissionsUtils.jsm',
     'PopupNotifications.jsm',
     'Preferences.jsm',