Bug 1147664 - Detailed mode for PerformanceStats (high-level). r=mossop
☠☠ backed out by 68596c2c0496 ☠ ☠
authorDavid Rajchenbach-Teller <dteller@mozilla.com>
Fri, 12 Jun 2015 21:52:06 +0200
changeset 254097 9eb2e0c7b864e31c7eac6999f7a74920a5dc344c
parent 254096 19bdfb755eebb79704eda17585583849433aea84
child 254098 fa418b120a3bd97d3571562909493f9e31403fdc
push id14168
push userryanvm@gmail.com
push dateWed, 22 Jul 2015 15:36:12 +0000
treeherderfx-team@9eb2e0c7b864 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmossop
bugs1147664
milestone42.0a1
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
--- 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);
   },
 
+  importChildren: function(parent, children) {
+    if (!Array.isArray(children)) {
+      throw new TypeError();
+    }
+    if (!parent || !(parent instanceof PerformanceData)) {
+      throw new TypeError();
+    }
+    return this._impl.importChildren(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;
-    }
+    },
+    importChildren: 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
       };
-    }
+    },
+    importChildren: 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,29 +294,29 @@ let Probes = {
     },
     isEqual: function(a, b) {
       return a.ticks == b.ticks;
     },
     subtract: function(a, b) {
       return {
         ticks: a.ticks - b.ticks
       };
-    }
+    },
+    importChildren: function() { /* nothing to do */ },
   }),
 
   "jank-content": new Probe("jank-content", {
     _isActive: false,
     set isActive(x) {
       this._isActive = x;
       if (x) {
         Process.broadcast("acquire", ["jank"]);
       } else {
         Process.broadcast("release", ["jank"]);
       }
-    },
     get isActive() {
       return this._isActive;
     },
     extract: function(xpcom) {
       return {};
     },
     isEqual: function(a, b) {
       return true;
@@ -318,48 +330,73 @@ let Probes = {
     _isActive: false,
     set isActive(x) {
       this._isActive = x;
       if (x) {
         Process.broadcast("acquire", ["cpow"]);
       } else {
         Process.broadcast("release", ["cpow"]);
       }
-    },
     get isActive() {
       return this._isActive;
     },
     extract: function(xpcom) {
       return {};
     },
     isEqual: function(a, b) {
       return true;
     },
     subtract: function(a, b) {
       return null;
     }
   }),
 
   "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;
     }
   }),
+
+  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;
+    },
+    importChildren: 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 +689,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.importChildren(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");
 });