Bug 1147664 - Detailed mode for PerformanceStats (high-level). r=mossop
authorDavid Rajchenbach-Teller <dteller@mozilla.com>
Fri, 12 Jun 2015 21:52:06 +0200
changeset 286407 1c0413eaf9f32e9a181d4fdbc5b3220f2cf5ab33
parent 286406 60d809c185ad3be1f0dda05c2010f5b4e914e638
child 286408 d593a1d313c6f365ed66432e4b7f06544a31ac68
push id5067
push userraliiev@mozilla.com
push dateMon, 21 Sep 2015 14:04:52 +0000
treeherdermozilla-beta@14221ffe5b2f [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmossop
bugs1147664
milestone42.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 1147664 - Detailed mode for PerformanceStats (high-level). r=mossop
toolkit/components/perfmonitoring/PerformanceStats.jsm
toolkit/components/perfmonitoring/nsIPerformanceStats.idl
toolkit/components/perfmonitoring/nsPerformanceStats.cpp
toolkit/components/perfmonitoring/tests/browser/browser_compartments.js
toolkit/components/perfmonitoring/tests/xpcshell/test_compartments.js
toolkit/components/perfmonitoring/tests/xpcshell/xpcshell.ini
--- a/toolkit/components/perfmonitoring/PerformanceStats.jsm
+++ b/toolkit/components/perfmonitoring/PerformanceStats.jsm
@@ -141,16 +141,26 @@ Probe.prototype = {
       throw new TypeError();
     }
     if (b == null) {
       return a;
     }
     return this._impl.subtract(a, b);
   },
 
+  importChildCompartments: function(parent, children) {
+    if (!Array.isArray(children)) {
+      throw new TypeError();
+    }
+    if (!parent || !(parent instanceof PerformanceData)) {
+      throw new TypeError();
+    }
+    return this._impl.importChildCompartments(parent, children);
+  },
+
   /**
    * The name of the probe.
    */
   get name() {
     return this._name;
   }
 };
 
@@ -223,17 +233,18 @@ let Probes = {
         durations: [],
         longestDuration: -1,
       };
       for (let i = 0; i < a.durations.length; ++i) {
         result.durations[i] = a.durations[i] - b.durations[i];
       }
       result.longestDuration = lastNonZero(result.durations);
       return result;
-    }
+    },
+    importChildCompartments: function() { /* nothing to do */ },
   }),
 
   /**
    * A probe measuring CPOW activity.
    *
    * Data provided by this probe uses the following format:
    *
    * @field {number} totalCPOWTime The amount of wallclock time
@@ -253,17 +264,18 @@ let Probes = {
     },
     isEqual: function(a, b) {
       return a.totalCPOWTime == b.totalCPOWTime;
     },
     subtract: function(a, b) {
       return {
         totalCPOWTime: a.totalCPOWTime - b.totalCPOWTime
       };
-    }
+    },
+    importChildCompartments: function() { /* nothing to do */ },
   }),
 
   /**
    * A probe measuring activations, i.e. the number
    * of times code execution has entered a given
    * PerformanceGroup.
    *
    * Note that this probe is always active.
@@ -282,17 +294,18 @@ let Probes = {
     },
     isEqual: function(a, b) {
       return a.ticks == b.ticks;
     },
     subtract: function(a, b) {
       return {
         ticks: a.ticks - b.ticks
       };
-    }
+    },
+    importChildCompartments: function() { /* nothing to do */ },
   }),
 
   "jank-content": new Probe("jank-content", {
     _isActive: false,
     set isActive(x) {
       this._isActive = x;
       if (x) {
         Process.broadcast("acquire", ["jank"]);
@@ -306,17 +319,18 @@ let Probes = {
     extract: function(xpcom) {
       return {};
     },
     isEqual: function(a, b) {
       return true;
     },
     subtract: function(a, b) {
       return null;
-    }
+    },
+    importChildCompartments: function() { /* nothing to do */ },
   }),
 
   "cpow-content": new Probe("cpow-content", {
     _isActive: false,
     set isActive(x) {
       this._isActive = x;
       if (x) {
         Process.broadcast("acquire", ["cpow"]);
@@ -330,36 +344,65 @@ let Probes = {
     extract: function(xpcom) {
       return {};
     },
     isEqual: function(a, b) {
       return true;
     },
     subtract: function(a, b) {
       return null;
-    }
+    },
+    importChildCompartments: function() { /* nothing to do */ },
   }),
 
   "ticks-content": new Probe("ticks-content", {
+    _isActive: false,
     set isActive(x) {
-      // Ignore: This probe is always active.
+      this._isActive = x;
+      if (x) {
+        Process.broadcast("acquire", ["ticks"]);
+      } else {
+        Process.broadcast("release", ["ticks"]);
+      }
     },
     get isActive() {
-      return true;
+      return this._isActive;
     },
     extract: function(xpcom) {
       return {};
     },
     isEqual: function(a, b) {
       return true;
     },
     subtract: function(a, b) {
       return null;
-    }
+    },
+    importChildCompartments: function() { /* nothing to do */ },
   }),
+
+  compartments: new Probe("compartments", {
+    set isActive(x) {
+      performanceStatsService.isMonitoringPerCompartment = x;
+    },
+    get isActive() {
+      return performanceStatsService.isMonitoringPerCompartment;
+    },
+    extract: function(xpcom) {
+      return null;
+    },
+    isEqual: function(a, b) {
+      return true;
+    },
+    subtract: function(a, b) {
+      return true;
+    },
+    importChildCompartments: function(parent, children) {
+      parent.children = children;
+    },
+  })
 };
 
 
 /**
  * A monitor for a set of probes.
  *
  * Keeping probes active when they are unused is often a bad
  * idea for performance reasons. Upon destruction, or whenever
@@ -652,26 +695,42 @@ function PerformanceDiff(current, old = 
  *
  * @param {nsIPerformanceSnapshot} xpcom The data acquired from this process.
  * @param {Array<Object>} childProcesses The data acquired from children processes.
  * @param {Array<Probe>} probes The active probes.
  */
 function Snapshot({xpcom, childProcesses, probes}) {
   this.componentsData = [];
 
-  // Parent process
+  // Current process
   if (xpcom) {
+    let children = new Map();
     let enumeration = xpcom.getComponentsData().enumerate();
     while (enumeration.hasMoreElements()) {
       let xpcom = enumeration.getNext().QueryInterface(Ci.nsIPerformanceStats);
-      this.componentsData.push(new PerformanceData({xpcom, probes}));
+      let stat = new PerformanceData({xpcom, probes});
+      if (!stat.parentId) {
+        this.componentsData.push(stat);
+      } else {
+        let siblings = children.get(stat.parentId);
+        if (!siblings) {
+          children.set(stat.parentId, (siblings = []));
+        }
+        siblings.push(stat);
+      }
+    }
+
+    for (let parent of this.componentsData) {
+      for (let probe of probes) {
+        probe.importChildCompartments(parent, children.get(parent.groupId) || []);
+      }
     }
   }
 
-  // Content processes
+  // Child processes
   if (childProcesses) {
     for (let {componentsData} of childProcesses) {
       // We are only interested in `componentsData` for the time being.
       for (let json of componentsData) {
         this.componentsData.push(new PerformanceData({json, probes}));
       }
     }
   }
--- a/toolkit/components/perfmonitoring/nsIPerformanceStats.idl
+++ b/toolkit/components/perfmonitoring/nsIPerformanceStats.idl
@@ -12,32 +12,42 @@
  * Mechanisms for querying the current process about performance
  * information.
  *
  * JavaScript clients should rather use PerformanceStats.jsm.
  */
 
 /**
  * Snapshot of the performance of a component, e.g. an add-on, a web
- * page, system built-ins, or the entire process itself.
+ * page, system built-ins, a module or the entire process itself.
  *
  * All values are monotonic and are updated only when
  * `nsIPerformanceStatsService.isStopwatchActive` is `true`.
  */
-[scriptable, uuid(47f8d36d-1d67-43cb-befd-d2f4720ac568)]
+[scriptable, uuid(1bc2d016-e9ae-4186-97c6-9478eddda245)]
 interface nsIPerformanceStats: nsISupports {
   /**
    * An identifier unique to the component.
    *
    * This identifier is somewhat human-readable to aid with debugging,
    * but clients should not rely upon the format.
    */
   readonly attribute AString groupId;
 
   /**
+   * If this component is part of a larger component, the larger
+   * component. Otherwise, null.
+   *
+   * As of this writing, there can be at most two levels of components:
+   * - compartments (a single module, iframe, etc.);
+   * - groups (an entire add-on, an entire webpage, etc.).
+   */
+  readonly attribute AString parentId;
+
+  /**
    * The name of the component:
    * - for the process itself, "<process>";
    * - for platform code, "<platform>";
    * - for an add-on, the identifier of the addon (e.g. "myaddon@foo.bar");
    * - for a webpage, the url of the page.
    */
   readonly attribute AString name;
 
@@ -107,29 +117,36 @@ interface nsIPerformanceSnapshot: nsISup
    * This contains the total amount of time spent executing JS code,
    * the total amount of time spent waiting for system calls while
    * executing JS code, the total amount of time performing blocking
    * inter-process calls, etc.
    */
   nsIPerformanceStats getProcessData();
 };
 
-[scriptable, builtinclass, uuid(0469e6af-95c3-4961-a385-4bc009128939)]
+[scriptable, builtinclass, uuid(60973d54-13e2-455c-a3c6-84dea5dfc8b9)]
 interface nsIPerformanceStatsService : nsISupports {
   /**
    * `true` if we should monitor CPOW, `false` otherwise.
    */
   [implicit_jscontext] attribute bool isMonitoringCPOW;
 
   /**
    * `true` if we should monitor jank, `false` otherwise.
    */
   [implicit_jscontext] attribute bool isMonitoringJank;
 
   /**
+   * `true` if all compartments need to be monitored individually,
+   * `false` if only performance groups (i.e. entire add-ons, entire
+   * webpages, etc.) need to be monitored.
+   */
+  [implicit_jscontext] attribute bool isMonitoringPerCompartment;
+
+  /**
    * Capture a snapshot of the performance data.
    */
   [implicit_jscontext] nsIPerformanceSnapshot getSnapshot();
 };
 
 %{C++
 #define NS_TOOLKIT_PERFORMANCESTATSSERVICE_CID {0xfd7435d4, 0x9ec4, 0x4699, \
       {0xad, 0xd4, 0x1b, 0xe8, 0x3d, 0xd6, 0x8e, 0xf3} }
--- a/toolkit/components/perfmonitoring/nsPerformanceStats.cpp
+++ b/toolkit/components/perfmonitoring/nsPerformanceStats.cpp
@@ -25,30 +25,35 @@
 #include "windows.h"
 #else
 #include <unistd.h>
 #endif
 
 class nsPerformanceStats: public nsIPerformanceStats {
 public:
   nsPerformanceStats(const nsAString& aName,
+                     nsIPerformanceStats* aParent,
                      const nsAString& aGroupId,
                      const nsAString& aAddonId,
                      const nsAString& aTitle,
                      const uint64_t aWindowId,
                      const bool aIsSystem,
                      const js::PerformanceData& aPerformanceData)
     : mName(aName)
     , mGroupId(aGroupId)
     , mAddonId(aAddonId)
     , mTitle(aTitle)
     , mWindowId(aWindowId)
     , mIsSystem(aIsSystem)
     , mPerformanceData(aPerformanceData)
   {
+    if (aParent) {
+      mozilla::DebugOnly<nsresult> rv = aParent->GetGroupId(mParentId);
+      MOZ_ASSERT(NS_SUCCEEDED(rv));
+    }
   }
   explicit nsPerformanceStats() {}
 
   NS_DECL_ISUPPORTS
 
   /* readonly attribute AString name; */
   NS_IMETHOD GetName(nsAString& aName) override {
     aName.Assign(mName);
@@ -56,16 +61,22 @@ public:
   };
 
   /* readonly attribute AString groupId; */
   NS_IMETHOD GetGroupId(nsAString& aGroupId) override {
     aGroupId.Assign(mGroupId);
     return NS_OK;
   };
 
+  /* readonly attribute AString parentId; */
+  NS_IMETHOD GetParentId(nsAString& aParentId) override {
+    aParentId.Assign(mParentId);
+    return NS_OK;
+  };
+
   /* readonly attribute AString addonId; */
   NS_IMETHOD GetAddonId(nsAString& aAddonId) override {
     aAddonId.Assign(mAddonId);
     return NS_OK;
   };
 
   /* readonly attribute uint64_t windowId; */
   NS_IMETHOD GetWindowId(uint64_t *aWindowId) override {
@@ -119,16 +130,17 @@ public:
     for (size_t i = 0; i < length; ++i) {
       (*aNumberOfOccurrences)[i] = mPerformanceData.durations[i];
     }
     return NS_OK;
   };
 
 private:
   nsString mName;
+  nsString mParentId;
   nsString mGroupId;
   nsString mAddonId;
   nsString mTitle;
   uint64_t mWindowId;
   bool mIsSystem;
 
   js::PerformanceData mPerformanceData;
 
@@ -154,45 +166,56 @@ private:
    *
    * Precondition: this method assumes that we have entered the JSCompartment for which data `c`
    * has been collected.
    *
    * `cx` may be `nullptr` if we are importing the statistics for the
    * entire process, rather than the statistics for a specific set of
    * compartments.
    */
-  already_AddRefed<nsIPerformanceStats> ImportStats(JSContext* cx, const js::PerformanceData& data, uint64_t uid);
+  already_AddRefed<nsIPerformanceStats> ImportStats(JSContext* cx, const js::PerformanceData& data, uint64_t uid, nsIPerformanceStats* parent);
 
   /**
    * Callbacks for iterating through the `PerformanceStats` of a runtime.
    */
-  bool IterPerformanceStatsCallbackInternal(JSContext* cx, const js::PerformanceData& stats, uint64_t uid);
-  static bool IterPerformanceStatsCallback(JSContext* cx, const js::PerformanceData& stats, uint64_t uid, void* self);
+  bool IterPerformanceStatsCallbackInternal(JSContext* cx,
+                                            const js::PerformanceData& ownStats, const uint64_t ownId,
+                                            const uint64_t* parentId);
+  static bool IterPerformanceStatsCallback(JSContext* cx,
+                                           const js::PerformanceData& ownStats, const uint64_t ownId,
+                                           const uint64_t* parentId,
+                                           void* self);
 
   // If the context represents a window, extract the title and window ID.
   // Otherwise, extract "" and 0.
   static void GetWindowData(JSContext*,
                             nsString& title,
                             uint64_t* windowId);
   void GetGroupId(JSContext*,
                   uint64_t uid,
                   nsString& groupId);
+
+  static void GetName(JSContext*,
+                      JS::Handle<JSObject*> global,
+                      nsString& name);
+
   // If the context presents an add-on, extract the addon ID.
   // Otherwise, extract "".
   static void GetAddonId(JSContext*,
                          JS::Handle<JSObject*> global,
                          nsAString& addonId);
 
   // Determine whether a context is part of the system principals.
   static bool GetIsSystem(JSContext*,
                           JS::Handle<JSObject*> global);
 
 private:
   nsCOMArray<nsIPerformanceStats> mComponentsData;
   nsCOMPtr<nsIPerformanceStats> mProcessData;
+  nsBaseHashtable<nsUint64HashKey, nsCOMPtr<nsIPerformanceStats>, nsCOMPtr<nsIPerformanceStats> > mCachedStats;
   uint64_t mProcessId;
 };
 
 NS_IMPL_ISUPPORTS(nsPerformanceSnapshot, nsIPerformanceSnapshot)
 
 nsPerformanceSnapshot::nsPerformanceSnapshot()
 {
 }
@@ -229,34 +252,16 @@ nsPerformanceSnapshot::GetWindowData(JSC
 
   nsCOMPtr<nsIDocument> doc = ptop->GetExtantDoc();
   if (!doc) {
     return;
   }
 
   doc->GetTitle(title);
   *windowId = ptop->WindowID();
-
-  *memoryUsed = -1;
-  int64_t jsObjectsSize;
-  int64_t jsStringsSize;
-  int64_t jsOtherSize;
-  int64_t domSize;
-  int64_t styleSize;
-  int64_t otherSize;
-  double jsMilliseconds;
-  double nonJSMilliseconds;
-  nsCOMPtr<nsIMemoryReporterManager> manager = nsMemoryReporterManager::GetOrCreate();
-  if (!manager) {
-    return;
-  }
-  nsresult rv = manager->GetSizeOfTab(top, &jsObjectsSize, &jsStringsSize, &jsOtherSize, &domSize, &styleSize, memoryUsed);
-  if (NS_FAILED(rv)) {
-    *memoryUsed = -1;
-  }
 }
 
 /* static */ void
 nsPerformanceSnapshot::GetAddonId(JSContext*,
                                   JS::Handle<JSObject*> global,
                                   nsAString& addonId)
 {
   addonId.AssignLiteral("");
@@ -286,18 +291,54 @@ nsPerformanceSnapshot::GetGroupId(JSCont
 
 /* static */ bool
 nsPerformanceSnapshot::GetIsSystem(JSContext*,
                                    JS::Handle<JSObject*> global)
 {
   return nsContentUtils::IsSystemPrincipal(nsContentUtils::ObjectPrincipal(global));
 }
 
+/* static */
+void
+nsPerformanceSnapshot::GetName(JSContext* cx,
+                               JS::Handle<JSObject*> global,
+                               nsString& name)
+{
+  nsAutoCString cname;
+  do {
+    // Attempt to use the URL as name.
+    nsCOMPtr<nsIPrincipal> principal = nsContentUtils::ObjectPrincipal(global);
+    if (!principal) {
+      break;
+    }
+
+    nsCOMPtr<nsIURI> uri;
+    nsresult rv = principal->GetURI(getter_AddRefs(uri));
+    if (NS_FAILED(rv) || !uri) {
+      break;
+    }
+
+    rv = uri->GetSpec(cname);
+    if (NS_FAILED(rv)) {
+      break;
+    }
+
+    name.Assign(NS_ConvertUTF8toUTF16(cname));
+    return;
+  } while(false);
+  xpc::GetCurrentCompartmentName(cx, cname);
+  name.Assign(NS_ConvertUTF8toUTF16(cname));
+}
+
 already_AddRefed<nsIPerformanceStats>
-nsPerformanceSnapshot::ImportStats(JSContext* cx, const js::PerformanceData& performance, const uint64_t uid) {
+nsPerformanceSnapshot::ImportStats(JSContext* cx, const js::PerformanceData& performance, const uint64_t uid, nsIPerformanceStats* parent) {
+  if (performance.ticks == 0) {
+    // Ignore compartments with no activity.
+    return nullptr;
+  }
   JS::RootedObject global(cx, JS::CurrentGlobalOrNull(cx));
 
   if (!global) {
     // While it is possible for a compartment to have no global
     // (e.g. atoms), this compartment is not very interesting for us.
     return nullptr;
   }
 
@@ -306,53 +347,63 @@ nsPerformanceSnapshot::ImportStats(JSCon
 
   nsString addonId;
   GetAddonId(cx, global, addonId);
 
   nsString title;
   uint64_t windowId;
   GetWindowData(cx, title, &windowId);
 
-  nsAutoString name;
-  nsAutoCString cname;
-  xpc::GetCurrentCompartmentName(cx, cname);
-  name.Assign(NS_ConvertUTF8toUTF16(cname));
+  nsString name;
+  GetName(cx, global, name);
 
   bool isSystem = GetIsSystem(cx, global);
 
   nsCOMPtr<nsIPerformanceStats> result =
-    new nsPerformanceStats(name, groupId, addonId, title, windowId, isSystem, performance);
+    new nsPerformanceStats(name, parent, groupId, addonId, title, windowId, isSystem, performance);
   return result.forget();
 }
 
 /*static*/ bool
-nsPerformanceSnapshot::IterPerformanceStatsCallback(JSContext* cx, const js::PerformanceData& stats, const uint64_t uid, void* self) {
-  return reinterpret_cast<nsPerformanceSnapshot*>(self)->IterPerformanceStatsCallbackInternal(cx, stats, uid);
+nsPerformanceSnapshot::IterPerformanceStatsCallback(JSContext* cx,
+                                                    const js::PerformanceData& stats, const uint64_t id,
+                                                    const uint64_t* parentId,
+                                                    void* self) {
+  return reinterpret_cast<nsPerformanceSnapshot*>(self)->IterPerformanceStatsCallbackInternal(cx, stats, id, parentId);
 }
 
 bool
-nsPerformanceSnapshot::IterPerformanceStatsCallbackInternal(JSContext* cx, const js::PerformanceData& stats, const uint64_t uid) {
-  nsCOMPtr<nsIPerformanceStats> result = ImportStats(cx, stats, uid);
+nsPerformanceSnapshot::IterPerformanceStatsCallbackInternal(JSContext* cx,
+                                                            const js::PerformanceData& stats, const uint64_t id,
+                                                            const uint64_t* parentId) {
+
+  nsCOMPtr<nsIPerformanceStats> parent = parentId ? mCachedStats.Get(*parentId) : nullptr;
+  nsCOMPtr<nsIPerformanceStats> result = ImportStats(cx, stats, id, parent);
   if (result) {
     mComponentsData.AppendElement(result);
+    mCachedStats.Put(id, result);
   }
 
   return true;
 }
 
 nsresult
 nsPerformanceSnapshot::Init(JSContext* cx, uint64_t processId) {
   mProcessId = processId;
   js::PerformanceData processStats;
   if (!js::IterPerformanceStats(cx, nsPerformanceSnapshot::IterPerformanceStatsCallback, &processStats, this)) {
     return NS_ERROR_UNEXPECTED;
   }
 
+  nsAutoString processGroupId;
+  processGroupId.AssignLiteral("process: ");
+  processGroupId.AppendInt(processId);
   mProcessData = new nsPerformanceStats(NS_LITERAL_STRING("<process>"), // name
-                                        NS_LITERAL_STRING("<process:?>"), // group id
+                                        nullptr,                        // parent
+                                        processGroupId,                 // group id
                                         NS_LITERAL_STRING(""),          // add-on id
                                         NS_LITERAL_STRING(""),          // title
                                         0,                              // window id
                                         true,                           // isSystem
                                         processStats);
   return NS_OK;
 }
 
@@ -418,16 +469,30 @@ NS_IMETHODIMP nsPerformanceStatsService:
 NS_IMETHODIMP nsPerformanceStatsService::SetIsMonitoringJank(JSContext* cx, bool aIsStopwatchActive)
 {
   JSRuntime *runtime = JS_GetRuntime(cx);
   if (!js::SetStopwatchIsMonitoringJank(runtime, aIsStopwatchActive)) {
     return NS_ERROR_OUT_OF_MEMORY;
   }
   return NS_OK;
 }
+NS_IMETHODIMP nsPerformanceStatsService::GetIsMonitoringPerCompartment(JSContext* cx, bool *aIsStopwatchActive)
+{
+  JSRuntime *runtime = JS_GetRuntime(cx);
+  *aIsStopwatchActive = js::GetStopwatchIsMonitoringPerCompartment(runtime);
+  return NS_OK;
+}
+NS_IMETHODIMP nsPerformanceStatsService::SetIsMonitoringPerCompartment(JSContext* cx, bool aIsStopwatchActive)
+{
+  JSRuntime *runtime = JS_GetRuntime(cx);
+  if (!js::SetStopwatchIsMonitoringPerCompartment(runtime, aIsStopwatchActive)) {
+    return NS_ERROR_OUT_OF_MEMORY;
+  }
+  return NS_OK;
+}
 
 /* readonly attribute nsIPerformanceSnapshot snapshot; */
 NS_IMETHODIMP nsPerformanceStatsService::GetSnapshot(JSContext* cx, nsIPerformanceSnapshot * *aSnapshot)
 {
   nsRefPtr<nsPerformanceSnapshot> snapshot = new nsPerformanceSnapshot();
   nsresult rv = snapshot->Init(cx, mProcessId);
   if (NS_FAILED(rv)) {
     return rv;
--- a/toolkit/components/perfmonitoring/tests/browser/browser_compartments.js
+++ b/toolkit/components/perfmonitoring/tests/browser/browser_compartments.js
@@ -23,17 +23,17 @@ function frameScript() {
     const { utils: Cu, classes: Cc, interfaces: Ci } = Components;
     Cu.import("resource://gre/modules/PerformanceStats.jsm");
 
     let performanceStatsService =
       Cc["@mozilla.org/toolkit/performance-stats-service;1"].
       getService(Ci.nsIPerformanceStatsService);
 
     // Make sure that the stopwatch is now active.
-    let monitor = PerformanceStats.getMonitor(["jank", "cpow", "ticks"]);
+    let monitor = PerformanceStats.getMonitor(["jank", "cpow", "ticks", "compartments"]);
 
     addMessageListener("compartments-test:getStatistics", () => {
       try {
         monitor.promiseSnapshot().then(snapshot => {
           sendAsyncMessage("compartments-test:getStatistics", snapshot);
         });
       } catch (ex) {
         Cu.reportError("Error in content (getStatistics): " + ex);
@@ -153,27 +153,47 @@ function monotinicity_tester(source, tes
     // Sanity check on components data.
     let map = new Map();
     for (let item of snapshot.componentsData) {
       for (let [probe, k] of [
         ["jank", "totalUserTime"],
         ["jank", "totalSystemTime"],
         ["cpow", "totalCPOWTime"]
       ]) {
-        SilentAssert.leq(item[probe][k], snapshot.processData[probe][k],
-          `Sanity check (${testName}): component has a lower ${k} than process`);
+        // Note that we cannot expect components data to be always smaller
+        // than process data, as `getrusage` & co are not monotonic.
+        SilentAssert.leq(item[probe][k], 2 * snapshot.processData[probe][k],
+          `Sanity check (${testName}): ${k} of component is not impossibly larger than that of process`);
       }
 
       let key = item.groupId;
       if (map.has(key)) {
         let old = map.get(key);
         Assert.ok(false, `Component ${key} has already been seen. Latest: ${item.title||item.addonId||item.name}, previous: ${old.title||old.addonId||old.name}`);
       }
       map.set(key, item);
     }
+    for (let item of snapshot.componentsData) {
+      if (!item.parentId) {
+        continue;
+      }
+      let parent = map.get(item.parentId);
+      SilentAssert.ok(parent, `The parent exists ${item.parentId}`);
+
+      for (let [probe, k] of [
+        ["jank", "totalUserTime"],
+        ["jank", "totalSystemTime"],
+        ["cpow", "totalCPOWTime"]
+      ]) {
+        // Note that we cannot expect components data to be always smaller
+        // than parent data, as `getrusage` & co are not monotonic.
+        SilentAssert.leq(item[probe][k], 2 * parent[probe][k],
+          `Sanity check (${testName}): ${k} of component is not impossibly larger than that of parent`);
+      }
+    }
     for (let [key, item] of map) {
       sanityCheck(previous.componentsMap.get(key), item);
       previous.componentsMap.set(key, item);
     }
   });
   let interval = window.setInterval(frameCheck, 300);
   registerCleanupFunction(() => {
     window.clearInterval(interval);
--- a/toolkit/components/perfmonitoring/tests/xpcshell/test_compartments.js
+++ b/toolkit/components/perfmonitoring/tests/xpcshell/test_compartments.js
@@ -14,37 +14,47 @@ let promiseStatistics = Task.async(funct
   yield Promise.resolve(); // Make sure that we wait until
   // statistics have been updated.
   let service = Cc["@mozilla.org/toolkit/performance-stats-service;1"].
     getService(Ci.nsIPerformanceStatsService);
   let snapshot = service.getSnapshot();
   let componentsData = [];
   let componentsEnum = snapshot.getComponentsData().enumerate();
   while (componentsEnum.hasMoreElements()) {
-    componentsData.push(componentsEnum.getNext().QueryInterface(Ci.nsIPerformanceStats));
+    let data = componentsEnum.getNext().QueryInterface(Ci.nsIPerformanceStats);
+    let normalized = JSON.parse(JSON.stringify(data));
+    componentsData.push(data);
   }
   return {
-    processData: snapshot.getProcessData(),
+    processData: JSON.parse(JSON.stringify(snapshot.getProcessData())),
     componentsData
   };
 });
 
 let promiseSetMonitoring = Task.async(function*(to) {
   let service = Cc["@mozilla.org/toolkit/performance-stats-service;1"].
     getService(Ci.nsIPerformanceStatsService);
   service.isMonitoringJank = to;
   service.isMonitoringCPOW = to;
   yield Promise.resolve();
 });
 
+let promiseSetPerCompartment = Task.async(function*(to) {
+  let service = Cc["@mozilla.org/toolkit/performance-stats-service;1"].
+    getService(Ci.nsIPerformanceStatsService);
+  service.isMonitoringPerCompartment = to;
+  yield Promise.resolve();
+});
+
 function getBuiltinStatistics(name, snapshot) {
   let stats = snapshot.componentsData.find(stats =>
     stats.isSystem && !stats.addonId
   );
   do_print(`Built-in statistics for ${name} were ${stats?"":"not "}found`);
+  do_print(JSON.stringify(snapshot.componentsData, null, "\t"));
   return stats;
 }
 
 function burnCPU(ms) {
   do_print("Burning CPU");
   let counter = 0;
   let ignored = [];
   let start = Date.now();
@@ -52,27 +62,35 @@ function burnCPU(ms) {
     ignored.push(0);
     ignored.shift();
     ++counter;
   }
   do_print("Burning CPU over, after " + counter + " iterations");
 }
 
 function ensureEquals(snap1, snap2, name) {
-  Assert.equal(
-    JSON.stringify(snap1.processData),
-    JSON.stringify(snap2.processData),
-    "Same process data: " + name);
+  for (let k of Object.keys(snap1.processData)) {
+    if (k == "ticks") {
+      // Ticks monitoring cannot be deactivated
+      continue;
+    }
+    Assert.equal(snap1.processData[k], snap2.processData[k], `Same process data value ${k} (${name})`)
+  }
   let stats1 = snap1.componentsData.sort((a, b) => a.name <= b.name);
   let stats2 = snap2.componentsData.sort((a, b) => a.name <= b.name);
-  Assert.equal(
-    JSON.stringify(stats1),
-    JSON.stringify(stats2),
-    "Same components data: " + name
-  );
+  Assert.equal(stats1.length, stats2.length, `Same number of components (${name})`);
+  for (let i = 0; i < stats1.length; ++i) {
+    for (let k of Object.keys(stats1[i])) {
+      if (k == "ticks") {
+        // Ticks monitoring cannot be deactivated
+        continue;
+      }
+      Assert.equal(stats1[i][k], stats1[i][k], `Same component data value ${i} ${k} (${name})`)
+    }
+  }
 }
 
 function hasLowPrecision() {
   let [sysName, sysVersion] = [Services.sysinfo.getPropertyAsAString("name"), Services.sysinfo.getPropertyAsDouble("version")];
   do_print(`Running ${sysName} version ${sysVersion}`);
 
   if (sysName == "Windows_NT" && sysVersion < 6) {
     do_print("Running old Windows, need to deactivate tests due to bad precision.");
@@ -83,16 +101,17 @@ function hasLowPrecision() {
     return true;
   }
   do_print("This platform has good precision.")
   return false;
 }
 
 add_task(function* test_measure() {
   let skipPrecisionTests = hasLowPrecision();
+  yield promiseSetPerCompartment(false);
 
   do_print("Burn CPU without the stopwatch");
   yield promiseSetMonitoring(false);
   let stats0 = yield promiseStatistics("Initial state");
   burnCPU(300);
   let stats1 = yield promiseStatistics("Initial state + burn, without stopwatch");
 
   do_print("Burn CPU with the stopwatch");
@@ -132,9 +151,19 @@ add_task(function* test_measure() {
     do_print("Skipping totalUserTime check under Windows XP, as timer is not always updated by the OS.")
   } else {
     Assert.ok(builtin2.totalUserTime - builtin1.totalUserTime >= 10000, `At least 10ms counted for built-in statistics (${builtin2.totalUserTime - builtin1.totalUserTime})`);
   }
   Assert.equal(builtin2.totalCPOWTime, builtin1.totalCPOWTime, "We haven't used any CPOW time during the first burn for the built-in");
   Assert.equal(builtin2.totalCPOWTime, builtin1.totalCPOWTime, "No CPOW for built-in statistics");
   Assert.equal(builtin4.totalUserTime, builtin3.totalUserTime, "After deactivating the stopwatch, we didn't count any time for the built-in");
   Assert.equal(builtin4.totalCPOWTime, builtin3.totalCPOWTime, "After deactivating the stopwatch, we didn't count any CPOW time for the built-in");
+
+  // Ideally, we should be able to look for test_compartments.js, but
+  // it doesn't have its own compartment.
+  for (let stats of [stats1, stats2, stats3, stats4]) {
+    Assert.ok(!stats.componentsData.find(x => x.name.includes("Task.jsm")), "At this stage, Task.jsm doesn't show up in the components data");
+  }
+  yield promiseSetPerCompartment(true);
+  burnCPU(300);
+  let stats5 = yield promiseStatistics("With per-compartment monitoring");
+  Assert.ok(stats5.componentsData.find(x => x.name.includes("Task.jsm")), "With per-compartment monitoring, test_compartments.js shows up");
 });
--- a/toolkit/components/perfmonitoring/tests/xpcshell/xpcshell.ini
+++ b/toolkit/components/perfmonitoring/tests/xpcshell/xpcshell.ini
@@ -1,5 +1,6 @@
 [DEFAULT]
 head=
 tail=
 
 [test_compartments.js]
+skip-if = toolkit == 'gonk' # Fails on b2g emulator, bug 1147664