Bug 674779 - Per-component CPU monitoring, low-level. r=blassey, r=jandem
authorDavid Rajchenbach-Teller <dteller@mozilla.com>
Tue, 31 Mar 2015 12:40:52 +0200
changeset 236988 ba375ae024b33f10dc2910058ee86cf63b4cba7a
parent 236987 66945c5c4a4014b0e01ab319fab54f1c810c6663
child 236989 dab7343222264558e1c99e06167bc9fc8c3bda4d
push id57844
push userryanvm@gmail.com
push dateWed, 01 Apr 2015 13:46:54 +0000
treeherdermozilla-inbound@b4efb3f1976f [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersblassey, jandem
bugs674779
milestone40.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 674779 - Per-component CPU monitoring, low-level. r=blassey, r=jandem
js/ipc/CPOWTimer.cpp
js/ipc/CPOWTimer.h
js/ipc/moz.build
js/public/Principals.h
js/src/frontend/TokenStream.cpp
js/src/jsapi.cpp
js/src/jsapi.h
js/src/jscntxtinlines.h
js/src/jscompartment.cpp
js/src/jscompartment.h
js/src/jsfriendapi.cpp
js/src/jsgc.cpp
js/src/jsscriptinlines.h
js/src/vm/HelperThreads.cpp
js/src/vm/Interpreter.cpp
js/src/vm/MemoryMetrics.cpp
js/src/vm/Runtime.cpp
js/src/vm/Runtime.h
js/src/vm/SavedStacks.cpp
js/src/vm/SelfHosting.cpp
js/src/vm/Stack-inl.h
js/src/vm/Stack.cpp
js/xpconnect/src/nsXPConnect.cpp
js/xpconnect/src/xpcprivate.h
--- a/js/ipc/CPOWTimer.cpp
+++ b/js/ipc/CPOWTimer.cpp
@@ -1,27 +1,25 @@
 /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 4 -*-
  * vim: set ts=4 sw=4 et 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 http://mozilla.org/MPL/2.0/. */
 
 #include "jsfriendapi.h"
-#include "xpcprivate.h"
+#include "nsContentUtils.h"
 #include "CPOWTimer.h"
 
 CPOWTimer::~CPOWTimer()
 {
-    /* This is a best effort to find the compartment responsible for this CPOW call */
-    nsIGlobalObject* global = mozilla::dom::GetIncumbentGlobal();
-    if (!global)
-        return;
-    JSObject* obj = global->GetGlobalJSObject();
-    if (!obj)
+    JSContext* cx = nsContentUtils::GetCurrentJSContextForThread();
+    if (!cx)
         return;
-    JSCompartment* compartment = js::GetObjectCompartment(obj);
-    xpc::CompartmentPrivate* compartmentPrivate = xpc::CompartmentPrivate::Get(compartment);
-    if (!compartmentPrivate)
+
+    JSRuntime* runtime = JS_GetRuntime(cx);
+    if (!js::IsStopwatchActive(runtime))
         return;
-    PRIntervalTime time = PR_IntervalNow() - startInterval;
-    compartmentPrivate->CPOWTime += time;
+
+    js::PerformanceData *performance = js::GetPerformanceData(runtime);
+    uint64_t duration = PR_IntervalToMicroseconds(PR_IntervalNow() - startInterval);
+    performance->totalCPOWTime += duration;
 }
--- a/js/ipc/CPOWTimer.h
+++ b/js/ipc/CPOWTimer.h
@@ -7,18 +7,36 @@
 
 #ifndef CPOWTIMER_H
 #define CPOWTIMER_H
 
 #include "prinrval.h"
 
 class JSObject;
 
-class MOZ_STACK_CLASS CPOWTimer {
+/**
+ * A stopwatch measuring the duration of a CPOW call.
+ *
+ * As the process is consuming neither user time nor system time
+ * during a CPOW call, we measure such durations using wallclock time.
+ *
+ * This stopwatch is active iff JSRuntime::stopwatch.isActive is set.
+ * Upon destruction, update JSRuntime::stopwatch.data.totalCPOWTime.
+ */
+class MOZ_STACK_CLASS CPOWTimer final {
   public:
-    CPOWTimer(): startInterval(PR_IntervalNow()) {}
+    explicit inline CPOWTimer(MOZ_GUARD_OBJECT_NOTIFIER_ONLY_PARAM)
+        : startInterval(PR_IntervalNow())
+    {
+        MOZ_GUARD_OBJECT_NOTIFIER_INIT;
+    }
     ~CPOWTimer();
 
   private:
+    MOZ_DECL_USE_GUARD_OBJECT_NOTIFIER
+
+    /**
+     * The instant at which the stopwatch was started.
+     */
     PRIntervalTime startInterval;
 };
 
 #endif
--- a/js/ipc/moz.build
+++ b/js/ipc/moz.build
@@ -30,12 +30,13 @@ EXPORTS.mozilla.jsipc = [
     'CpowHolder.h',
     'CrossProcessObjectWrappers.h',
 ]
 
 LOCAL_INCLUDES += [
     '/dom/base',
     '/js/ipc',
     '/js/public',
+    '/js/src',
     '/js/xpconnect/src',
     '/js/xpconnect/wrappers',
 ]
 
--- a/js/public/Principals.h
+++ b/js/public/Principals.h
@@ -10,16 +10,20 @@
 #define js_Principals_h
 
 #include "mozilla/Atomics.h"
 
 #include <stdint.h>
 
 #include "jspubtd.h"
 
+namespace js {
+    struct PerformanceGroup;
+}
+
 struct JSPrincipals {
     /* Don't call "destroy"; use reference counting macros below. */
     mozilla::Atomic<int32_t> refcount;
 
 #ifdef JS_DEBUG
     /* A helper to facilitate principals debugging. */
     uint32_t    debugToken;
 #endif
--- a/js/src/frontend/TokenStream.cpp
+++ b/js/src/frontend/TokenStream.cpp
@@ -653,17 +653,17 @@ TokenStream::reportCompileErrorNumberVA(
         err.report.column = srcCoords.columnIndex(offset);
     }
 
     // If we have no location information, try to get one from the caller.
     bool callerFilename = false;
     if (offset != NoOffset && !err.report.filename && cx->isJSContext()) {
         NonBuiltinFrameIter iter(cx->asJSContext(),
                                  FrameIter::ALL_CONTEXTS, FrameIter::GO_THROUGH_SAVED,
-                                 cx->compartment()->principals);
+                                 cx->compartment()->principals());
         if (!iter.done() && iter.scriptFilename()) {
             callerFilename = true;
             err.report.filename = iter.scriptFilename();
             err.report.lineno = iter.computeLine(&err.report.column);
         }
     }
 
     err.argumentsType = (flags & JSREPORT_UC) ? ArgumentsAreUnicode : ArgumentsAreASCII;
--- a/js/src/jsapi.cpp
+++ b/js/src/jsapi.cpp
@@ -265,40 +265,88 @@ JS_GetEmptyStringValue(JSContext* cx)
 
 JS_PUBLIC_API(JSString*)
 JS_GetEmptyString(JSRuntime* rt)
 {
     MOZ_ASSERT(rt->hasContexts());
     return rt->emptyString;
 }
 
-JS_PUBLIC_API(bool)
-JS_GetCompartmentStats(JSRuntime* rt, CompartmentStatsVector& stats)
-{
+namespace js {
+
+JS_PUBLIC_API(bool)
+GetPerformanceStats(JSRuntime* rt,
+                    PerformanceStatsVector& stats,
+                    PerformanceStats& processStats)
+{
+    // As a PerformanceGroup is typically associated to several
+    // compartments, use a HashSet to make sure that we only report
+    // each PerformanceGroup once.
+    typedef HashSet<js::PerformanceGroup*,
+                    js::DefaultHasher<js::PerformanceGroup*>,
+                    js::SystemAllocPolicy> Set;
+    Set set;
+    if (!set.init(100)) {
+        return false;
+    }
+
     for (CompartmentsIter c(rt, WithAtoms); !c.done(); c.next()) {
-        if (!stats.growBy(1))
+        JSCompartment* compartment = c.get();
+        if (!compartment->performanceMonitoring.isLinked()) {
+            // Don't report compartments that do not even have a PerformanceGroup.
+            continue;
+        }
+        PerformanceGroup* group = compartment->performanceMonitoring.getGroup();
+
+        if (group->data.ticks == 0) {
+            // Don't report compartments that have never been used.
+            continue;
+        }
+
+        Set::AddPtr ptr = set.lookupForAdd(group);
+        if (ptr) {
+            // Don't report the same group twice.
+            continue;
+        }
+
+        if (!stats.growBy(1)) {
+            // Memory issue
             return false;
-
-        CompartmentTimeStats* stat = &stats.back();
-        stat->time = c.get()->totalTime;
-        stat->compartment = c.get();
-        stat->addonId = c.get()->addonId;
-        if (rt->compartmentNameCallback) {
-            (*rt->compartmentNameCallback)(rt, stat->compartment,
-                                           stat->compartmentName,
-                                           MOZ_ARRAY_LENGTH(stat->compartmentName));
+        }
+        PerformanceStats* stat = &stats.back();
+        stat->isSystem = compartment->isSystem();
+        if (compartment->addonId)
+            stat->addonId = compartment->addonId;
+
+        if (compartment->addonId || !compartment->isSystem()) {
+            if (rt->compartmentNameCallback) {
+                (*rt->compartmentNameCallback)(rt, compartment,
+                                               stat->name,
+                                               mozilla::ArrayLength(stat->name));
+            } else {
+                strcpy(stat->name, "<unknown>");
+            }
         } else {
-            strcpy(stat->compartmentName, "<unknown>");
+            strcpy(stat->name, "<platform>");
+        }
+        stat->performance = group->data;
+        if (!set.add(ptr, group)) {
+            // Memory issue
+            return false;
         }
     }
+
+    strcpy(processStats.name, "<process>");
+    processStats.addonId = nullptr;
+    processStats.isSystem = true;
+    processStats.performance = rt->stopwatch.performance;
+
     return true;
 }
 
-namespace js {
-
 void
 AssertHeapIsIdle(JSRuntime* rt)
 {
     MOZ_ASSERT(!rt->isHeapBusy());
 }
 
 void
 AssertHeapIsIdle(JSContext* cx)
--- a/js/src/jsapi.h
+++ b/js/src/jsapi.h
@@ -967,43 +967,30 @@ extern JS_PUBLIC_API(jsval)
 JS_GetPositiveInfinityValue(JSContext* cx);
 
 extern JS_PUBLIC_API(jsval)
 JS_GetEmptyStringValue(JSContext* cx);
 
 extern JS_PUBLIC_API(JSString*)
 JS_GetEmptyString(JSRuntime* rt);
 
-struct CompartmentTimeStats {
-    char compartmentName[1024];
-    JSAddonId* addonId;
-    JSCompartment* compartment;
-    uint64_t time;  // microseconds
-    uint64_t cpowTime; // microseconds
-};
-
-typedef js::Vector<CompartmentTimeStats, 0, js::SystemAllocPolicy> CompartmentStatsVector;
-
-extern JS_PUBLIC_API(bool)
-JS_GetCompartmentStats(JSRuntime* rt, CompartmentStatsVector& stats);
-
 extern JS_PUBLIC_API(bool)
 JS_ValueToObject(JSContext* cx, JS::HandleValue v, JS::MutableHandleObject objp);
 
 extern JS_PUBLIC_API(JSFunction*)
 JS_ValueToFunction(JSContext* cx, JS::HandleValue v);
 
 extern JS_PUBLIC_API(JSFunction*)
 JS_ValueToConstructor(JSContext* cx, JS::HandleValue v);
 
 extern JS_PUBLIC_API(JSString*)
 JS_ValueToSource(JSContext* cx, JS::Handle<JS::Value> v);
 
 extern JS_PUBLIC_API(bool)
-JS_DoubleIsInt32(double d, int32_t* ip);
+JS_DoubleIsInt32(double d, int32_t *ip);
 
 extern JS_PUBLIC_API(JSType)
 JS_TypeOfValue(JSContext* cx, JS::Handle<JS::Value> v);
 
 extern JS_PUBLIC_API(bool)
 JS_StrictlyEqual(JSContext* cx, JS::Handle<JS::Value> v1, JS::Handle<JS::Value> v2, bool* equal);
 
 extern JS_PUBLIC_API(bool)
@@ -5268,9 +5255,263 @@ GetSavedFrameParent(JSContext* cx, Handl
  * doesn't need to be in stack's compartment, and stack can be null, a
  * SavedFrame object, or a wrapper (CCW or Xray) around a SavedFrame object.
  */
 extern JS_PUBLIC_API(bool)
 BuildStackString(JSContext* cx, HandleObject stack, MutableHandleString stringp);
 
 } /* namespace JS */
 
+
+/* Stopwatch-based CPU monitoring. */
+
+namespace js {
+
+struct AutoStopwatch;
+
+// Container for performance data
+// All values are monotonic.
+struct PerformanceData {
+    // Number of times we have spent at least 2^n consecutive
+    // milliseconds executing code in this group.
+    // durations[0] is increased whenever we spend at least 1 ms
+    // executing code in this group
+    // durations[1] whenever we spend 2ms+
+    //
+    // durations[i] whenever we spend 2^ims+
+    uint64_t durations[10];
+
+    // Total amount of time spent executing code in this group, in
+    // microseconds.
+    uint64_t totalUserTime;
+    uint64_t totalSystemTime;
+    uint64_t totalCPOWTime;
+
+    // Total number of times code execution entered this group,
+    // since process launch. This may be greater than the number
+    // of times we have entered the event loop.
+    uint64_t ticks;
+
+    PerformanceData()
+      : totalUserTime(0)
+      , totalSystemTime(0)
+      , totalCPOWTime(0)
+      , ticks(0)
+    {
+        mozilla::PodArrayZero(durations);
+    }
+    PerformanceData(const PerformanceData& from)
+      : totalUserTime(from.totalUserTime)
+      , totalSystemTime(from.totalSystemTime)
+      , totalCPOWTime(from.totalCPOWTime)
+      , ticks(from.ticks)
+    {
+        mozilla::PodArrayCopy(durations, from.durations);
+    }
+    PerformanceData& operator=(const PerformanceData& from)
+    {
+        mozilla::PodArrayCopy(durations, from.durations);
+        totalUserTime = from.totalUserTime;
+        totalSystemTime = from.totalSystemTime;
+        totalCPOWTime = from.totalCPOWTime;
+        ticks = from.ticks;
+        return *this;
+    }
+};
+
+// A group of compartments forming a single unit in terms of
+// performance monitoring.
+//
+// Two compartments belong to the same group if either:
+// - they are part of the same add-on;
+// - they are part of the same webpage;
+// - they are both system built-ins.
+//
+// This class is refcounted by instances of `JSCompartment`.
+// Do not attempt to hold to a pointer to a `PerformanceGroup`.
+struct PerformanceGroup {
+
+    // Performance data for this group.
+    PerformanceData data;
+
+    // `true` if an instance of `AutoStopwatch` is already monitoring
+    // the performance of this performance group for this iteration
+    // of the event loop, `false` otherwise.
+    bool hasStopwatch(uint64_t iteration) const {
+        return stopwatch_ != nullptr && iteration_ == iteration;
+    }
+
+    // Mark that an instance of `AutoStopwatch` is monitoring
+    // the performance of this group for a given iteration.
+    void acquireStopwatch(uint64_t iteration, const AutoStopwatch *stopwatch) {
+        iteration_ = iteration;
+        stopwatch_ = stopwatch;
+    }
+
+    // Mark that no `AutoStopwatch` is monitoring the
+    // performance of this group for the iteration.
+    void releaseStopwatch(uint64_t iteration, const AutoStopwatch *stopwatch) {
+        if (iteration_ != iteration)
+            return;
+
+        MOZ_ASSERT(stopwatch == stopwatch_ || stopwatch_ == nullptr);
+        stopwatch_ = nullptr;
+    }
+
+    PerformanceGroup()
+      : stopwatch_(nullptr)
+      , iteration_(0)
+      , refCount_(0)
+    { }
+    ~PerformanceGroup()
+    {
+        MOZ_ASSERT(refCount_ == 0);
+    }
+  private:
+    PerformanceGroup& operator=(const PerformanceGroup&) = delete;
+    PerformanceGroup(const PerformanceGroup&) = delete;
+
+    // The stopwatch currently monitoring the group,
+    // or `nullptr` if none. Used ony for comparison.
+    const AutoStopwatch *stopwatch_;
+
+    // The current iteration of the event loop. If necessary,
+    // may safely overflow.
+    uint64_t iteration_;
+
+    // Increment/decrement the refcounter, return the updated value.
+    uint64_t incRefCount() {
+        MOZ_ASSERT(refCount_ + 1 > 0);
+        return ++refCount_;
+    }
+    uint64_t decRefCount() {
+        MOZ_ASSERT(refCount_ > 0);
+        return --refCount_;
+    }
+    friend struct PerformanceGroupHolder;
+
+  private:
+    // A reference counter. Maintained by PerformanceGroupHolder.
+    uint64_t refCount_;
+};
+
+//
+// Indirection towards a PerformanceGroup.
+// This structure handles reference counting for instances of PerformanceGroup.
+//
+struct PerformanceGroupHolder {
+    // Get the group.
+    // On first call, this causes a single Hashtable lookup.
+    // Successive calls do not require further lookups.
+    js::PerformanceGroup *getGroup();
+
+    // `true` if the this holder is currently associated to a
+    // PerformanceGroup, `false` otherwise. Use this method to avoid
+    // instantiating a PerformanceGroup if you only need to get
+    // available performance data.
+    inline bool isLinked() const {
+        return group_ != nullptr;
+    }
+
+    // Remove the link to the PerformanceGroup. This method is designed
+    // as an invalidation mechanism if the JSCompartment changes nature
+    // (new values of `isSystem()`, `principals()` or `addonId`).
+    void unlink();
+
+    PerformanceGroupHolder(JSRuntime *runtime, JSCompartment *compartment)
+      : runtime_(runtime)
+      , compartment_(compartment)
+      , group_(nullptr)
+    {   }
+    ~PerformanceGroupHolder();
+private:
+    // Return the key representing this PerformanceGroup in
+    // Runtime::Stopwatch.
+    // Do not deallocate the key.
+    void* getHashKey();
+
+    JSRuntime *runtime_;
+    JSCompartment *compartment_;
+
+    // The PerformanceGroup held by this object.
+    // Initially set to `nullptr` until the first cal to `getGroup`.
+    // May be reset to `nullptr` by a call to `unlink`.
+    js::PerformanceGroup *group_;
+};
+
+/**
+ * Reset any stopwatch currently measuring.
+ *
+ * This function is designed to be called when we process a new event.
+ */
+extern JS_PUBLIC_API(void)
+ResetStopwatches(JSRuntime*);
+
+/**
+ * Turn on/off stopwatch-based CPU monitoring.
+ *
+ * `SetStopwatchActive` may return `false` if monitoring could not be
+ * activated, which may happen if we are out of memory.
+ */
+extern JS_PUBLIC_API(bool)
+SetStopwatchActive(JSRuntime*, bool);
+extern JS_PUBLIC_API(bool)
+IsStopwatchActive(JSRuntime*);
+
+/**
+ * Access the performance information stored in a compartment.
+ */
+extern JS_PUBLIC_API(PerformanceData*)
+GetPerformanceData(JSRuntime*);
+
+/**
+ * Performance statistics for a performance group (a process, an
+ * add-on, a webpage, the built-ins or a special compartment).
+ */
+struct PerformanceStats {
+    /**
+     * If this group represents an add-on, the ID of the addon,
+     * otherwise `nullptr`.
+     */
+    JSAddonId *addonId;
+
+    /**
+     * If this group represents a webpage, the process itself or a special
+     * compartment, a human-readable name. Unspecified for add-ons.
+     */
+    char name[1024];
+
+    /**
+     * `true` if the group represents in system compartments, `false`
+     * otherwise. A group may never contain both system and non-system
+     * compartments.
+     */
+    bool isSystem;
+
+    /**
+     * Performance information.
+     */
+    js::PerformanceData performance;
+
+    PerformanceStats()
+      : addonId(nullptr)
+      , isSystem(false)
+    {
+        name[0] = '\0';
+    }
+};
+
+typedef js::Vector<PerformanceStats, 0, js::SystemAllocPolicy> PerformanceStatsVector;
+
+    /**
+ * Extract the performance statistics.
+ *
+ * After a successful call, `stats` holds the `PerformanceStats` for
+ * all performance groups, and `global` holds a `PerformanceStats`
+ * representing the entire process.
+ */
+extern JS_PUBLIC_API(bool)
+GetPerformanceStats(JSRuntime *rt, js::PerformanceStatsVector &stats, js::PerformanceStats &global);
+
+} /* namespace js */
+
+
 #endif /* jsapi_h */
--- a/js/src/jscntxtinlines.h
+++ b/js/src/jscntxtinlines.h
@@ -375,17 +375,17 @@ JSContext::setPendingException(js::Value
     // We don't use assertSameCompartment here to allow
     // js::SetPendingExceptionCrossContext to work.
     MOZ_ASSERT_IF(v.isObject(), v.toObject().compartment() == compartment());
 }
 
 inline bool
 JSContext::runningWithTrustedPrincipals() const
 {
-    return !compartment() || compartment()->principals == runtime()->trustedPrincipals();
+    return !compartment() || compartment()->principals() == runtime()->trustedPrincipals();
 }
 
 inline void
 js::ExclusiveContext::enterCompartment(JSCompartment* c)
 {
     enterCompartmentDepth_++;
     c->enter();
     setCompartment(c);
--- a/js/src/jscompartment.cpp
+++ b/js/src/jscompartment.cpp
@@ -36,29 +36,29 @@ using namespace js::jit;
 
 using mozilla::DebugOnly;
 using mozilla::PodArrayZero;
 
 JSCompartment::JSCompartment(Zone* zone, const JS::CompartmentOptions& options = JS::CompartmentOptions())
   : options_(options),
     zone_(zone),
     runtime_(zone->runtimeFromMainThread()),
-    principals(nullptr),
-    isSystem(false),
+    principals_(nullptr),
+    isSystem_(false),
     isSelfHosting(false),
     marked(true),
     warnedAboutNoSuchMethod(false),
     warnedAboutFlagsArgument(false),
     addonId(options.addonIdOrNull()),
 #ifdef DEBUG
     firedOnNewGlobalObject(false),
 #endif
     global_(nullptr),
     enterCompartmentDepth(0),
-    totalTime(0),
+    performanceMonitoring(runtime_, this),
     data(nullptr),
     objectMetadataCallback(nullptr),
     lastAnimationTime(0),
     regExps(runtime_),
     globalWriteBarriered(false),
     neuteredTypedObjects(0),
     propertyTree(thisForCtor()),
     selfHostingScriptSource(nullptr),
@@ -866,30 +866,30 @@ JSCompartment::addSizeOfIncludingThis(mo
     *regexpCompartment += regExps.sizeOfExcludingThis(mallocSizeOf);
     *savedStacksSet += savedStacks_.sizeOfExcludingThis(mallocSizeOf);
 }
 
 void
 JSCompartment::reportTelemetry()
 {
     // Only report telemetry for web content, not add-ons or chrome JS.
-    if (addonId || isSystem)
+    if (addonId || isSystem_)
         return;
 
     // Hazard analysis can't tell that the telemetry callbacks don't GC.
     JS::AutoSuppressGCAnalysis nogc;
 
     // Call back into Firefox's Telemetry reporter.
     for (size_t i = 0; i < DeprecatedLanguageExtensionCount; i++) {
         if (sawDeprecatedLanguageExtension[i])
             runtime_->addTelemetry(JS_TELEMETRY_DEPRECATED_LANGUAGE_EXTENSIONS_IN_CONTENT, i);
     }
 }
 
 void
 JSCompartment::addTelemetry(const char* filename, DeprecatedLanguageExtension e)
 {
     // Only report telemetry for web content, not add-ons or chrome JS.
-    if (addonId || isSystem || !filename || strncmp(filename, "http", 4) != 0)
+    if (addonId || isSystem_ || !filename || strncmp(filename, "http", 4) != 0)
         return;
 
     sawDeprecatedLanguageExtension[e] = true;
 }
--- a/js/src/jscompartment.h
+++ b/js/src/jscompartment.h
@@ -139,26 +139,68 @@ struct JSCompartment
 {
     JS::CompartmentOptions       options_;
 
   private:
     JS::Zone*                    zone_;
     JSRuntime*                   runtime_;
 
   public:
-    JSPrincipals*                principals;
-    bool                         isSystem;
+    /*
+     * The principals associated with this compartment. Note that the
+     * same several compartments may share the same principals and
+     * that a compartment may change principals during its lifetime
+     * (e.g. in case of lazy parsing).
+     */
+    inline JSPrincipals* principals() {
+        return principals_;
+    }
+    inline void setPrincipals(JSPrincipals* principals) {
+        if (principals_ == principals)
+            return;
+
+        // If we change principals, we need to unlink immediately this
+        // compartment from its PerformanceGroup. For one thing, the
+        // performance data we collect should not be improperly associated
+        // with a group to which we do not belong anymore. For another thing,
+        // we use `principals()` as part of the key to map compartments
+        // to a `PerformanceGroup`, so if we do not unlink now, this will
+        // be too late once we have updated `principals_`.
+        performanceMonitoring.unlink();
+        principals_ = principals;
+    }
+    inline bool isSystem() const {
+        return isSystem_;
+    }
+    inline void setIsSystem(bool isSystem) {
+        if (isSystem_ == isSystem)
+            return;
+
+        // If we change `isSystem*(`, we need to unlink immediately this
+        // compartment from its PerformanceGroup. For one thing, the
+        // performance data we collect should not be improperly associated
+        // to a group to which we do not belong anymore. For another thing,
+        // we use `isSystem()` as part of the key to map compartments
+        // to a `PerformanceGroup`, so if we do not unlink now, this will
+        // be too late once we have updated `isSystem_`.
+        performanceMonitoring.unlink();
+        isSystem_ = isSystem;
+    }
+  private:
+    JSPrincipals*                principals_;
+    bool                         isSystem_;
+  public:
     bool                         isSelfHosting;
     bool                         marked;
     bool                         warnedAboutNoSuchMethod;
     bool                         warnedAboutFlagsArgument;
 
     // A null add-on ID means that the compartment is not associated with an
     // add-on.
-    JSAddonId*                   addonId;
+    JSAddonId*                   const addonId;
 
 #ifdef DEBUG
     bool                         firedOnNewGlobalObject;
 #endif
 
     void mark() { marked = true; }
 
   private:
@@ -166,28 +208,23 @@ struct JSCompartment
     friend struct JSContext;
     friend class js::ExclusiveContext;
     js::ReadBarrieredGlobalObject global_;
 
     unsigned                     enterCompartmentDepth;
     int64_t                      startInterval;
 
   public:
-    int64_t                      totalTime;
+    js::PerformanceGroupHolder performanceMonitoring;
+
     void enter() {
-        if (addonId && !enterCompartmentDepth) {
-            startInterval = PRMJ_Now();
-        }
         enterCompartmentDepth++;
     }
     void leave() {
         enterCompartmentDepth--;
-        if (addonId && !enterCompartmentDepth) {
-            totalTime += (PRMJ_Now() - startInterval);
-        }
     }
     bool hasBeenEntered() { return !!enterCompartmentDepth; }
 
     JS::Zone* zone() { return zone_; }
     const JS::Zone* zone() const { return zone_; }
     JS::CompartmentOptions& options() { return options_; }
     const JS::CompartmentOptions& options() const { return options_; }
 
--- a/js/src/jsfriendapi.cpp
+++ b/js/src/jsfriendapi.cpp
@@ -151,50 +151,50 @@ JS_NewObjectWithoutMetadata(JSContext* c
     // Use an AutoEnterAnalysis to suppress invocation of the metadata callback.
     AutoEnterAnalysis enter(cx);
     return JS_NewObjectWithGivenProto(cx, clasp, proto);
 }
 
 JS_FRIEND_API(JSPrincipals*)
 JS_GetCompartmentPrincipals(JSCompartment* compartment)
 {
-    return compartment->principals;
+    return compartment->principals();
 }
 
 JS_FRIEND_API(void)
 JS_SetCompartmentPrincipals(JSCompartment* compartment, JSPrincipals* principals)
 {
     // Short circuit if there's no change.
-    if (principals == compartment->principals)
+    if (principals == compartment->principals())
         return;
 
     // Any compartment with the trusted principals -- and there can be
     // multiple -- is a system compartment.
     const JSPrincipals* trusted = compartment->runtimeFromMainThread()->trustedPrincipals();
     bool isSystem = principals && principals == trusted;
 
     // Clear out the old principals, if any.
-    if (compartment->principals) {
-        JS_DropPrincipals(compartment->runtimeFromMainThread(), compartment->principals);
-        compartment->principals = nullptr;
+    if (compartment->principals()) {
+        JS_DropPrincipals(compartment->runtimeFromMainThread(), compartment->principals());
+        compartment->setPrincipals(nullptr);
         // We'd like to assert that our new principals is always same-origin
         // with the old one, but JSPrincipals doesn't give us a way to do that.
         // But we can at least assert that we're not switching between system
         // and non-system.
-        MOZ_ASSERT(compartment->isSystem == isSystem);
+        MOZ_ASSERT(compartment->isSystem() == isSystem);
     }
 
     // Set up the new principals.
     if (principals) {
         JS_HoldPrincipals(principals);
-        compartment->principals = principals;
+        compartment->setPrincipals(principals);
     }
 
     // Update the system flag.
-    compartment->isSystem = isSystem;
+    compartment->setIsSystem(isSystem);
 }
 
 JS_FRIEND_API(JSPrincipals*)
 JS_GetScriptPrincipals(JSScript* script)
 {
     return script->principals();
 }
 
@@ -275,17 +275,17 @@ JS_FRIEND_API(JS::Zone*)
 js::GetCompartmentZone(JSCompartment* comp)
 {
     return comp->zone();
 }
 
 JS_FRIEND_API(bool)
 js::IsSystemCompartment(JSCompartment* comp)
 {
-    return comp->isSystem;
+    return comp->isSystem();
 }
 
 JS_FRIEND_API(bool)
 js::IsSystemZone(Zone* zone)
 {
     return zone->isSystem;
 }
 
--- a/js/src/jsgc.cpp
+++ b/js/src/jsgc.cpp
@@ -3524,18 +3524,18 @@ Zone::sweepCompartments(FreeOp* fop, boo
         /*
          * Don't delete the last compartment if all the ones before it were
          * deleted and keepAtleastOne is true.
          */
         bool dontDelete = read == end && !foundOne && keepAtleastOne;
         if ((!comp->marked && !dontDelete) || destroyingRuntime) {
             if (callback)
                 callback(fop, comp);
-            if (comp->principals)
-                JS_DropPrincipals(rt, comp->principals);
+            if (comp->principals())
+                JS_DropPrincipals(rt, comp->principals());
             js_delete(comp);
         } else {
             *write++ = comp;
             foundOne = true;
         }
     }
     compartments.resize(write - compartments.begin());
     MOZ_ASSERT_IF(keepAtleastOne, !compartments.empty());
--- a/js/src/jsscriptinlines.h
+++ b/js/src/jsscriptinlines.h
@@ -137,17 +137,17 @@ JSScript::global() const
      * can assert that maybeGlobal is non-null here.
      */
     return *compartment()->maybeGlobal();
 }
 
 inline JSPrincipals*
 JSScript::principals()
 {
-    return compartment()->principals;
+    return compartment()->principals();
 }
 
 inline void
 JSScript::setBaselineScript(JSContext* maybecx, js::jit::BaselineScript* baselineScript)
 {
     if (hasBaselineScript())
         js::jit::BaselineScript::writeBarrierPre(zone(), baseline);
     MOZ_ASSERT(!hasIonScript());
--- a/js/src/vm/HelperThreads.cpp
+++ b/js/src/vm/HelperThreads.cpp
@@ -331,17 +331,17 @@ js::StartOffThreadParseScript(JSContext*
     // Don't falsely inherit the host's global trace hook.
     compartmentOptions.setTrace(nullptr);
 
     JSObject* global = JS_NewGlobalObject(cx, &parseTaskGlobalClass, nullptr,
                                           JS::FireOnNewGlobalHook, compartmentOptions);
     if (!global)
         return false;
 
-    JS_SetCompartmentPrincipals(global->compartment(), cx->compartment()->principals);
+    JS_SetCompartmentPrincipals(global->compartment(), cx->compartment()->principals());
 
     RootedObject obj(cx);
 
     // Initialize all classes needed for parsing while we are still on the main
     // thread. Do this for both the target and the new global so that prototype
     // pointers can be changed infallibly after parsing finishes.
     if (!GetBuiltinConstructor(cx, JSProto_Function, &obj) ||
         !GetBuiltinConstructor(cx, JSProto_Array, &obj) ||
--- a/js/src/vm/Interpreter.cpp
+++ b/js/src/vm/Interpreter.cpp
@@ -5,16 +5,17 @@
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 /*
  * JavaScript bytecode interpreter.
  */
 
 #include "vm/Interpreter-inl.h"
 
+#include "mozilla/ArrayUtils.h"
 #include "mozilla/DebugOnly.h"
 #include "mozilla/FloatingPoint.h"
 #include "mozilla/PodOperations.h"
 
 #include <string.h>
 
 #include "jsarray.h"
 #include "jsatom.h"
@@ -47,16 +48,23 @@
 
 #include "jit/JitFrames-inl.h"
 #include "vm/Debugger-inl.h"
 #include "vm/NativeObject-inl.h"
 #include "vm/Probes-inl.h"
 #include "vm/ScopeObject-inl.h"
 #include "vm/Stack-inl.h"
 
+#if defined(XP_UNIX)
+#include <sys/resource.h>
+#elif defined(XP_WIN)
+#include <Processthreadsapi.h>
+#include <Windows.h>
+#endif // defined(XP_UNIX) || defined(XP_WIN)
+
 using namespace js;
 using namespace js::gc;
 
 using mozilla::DebugOnly;
 using mozilla::NumberEqualsInt32;
 using mozilla::PodCopy;
 using JS::ForOfIterator;
 
@@ -377,22 +385,244 @@ InvokeState::pushInterpreterFrame(JSCont
 }
 
 InterpreterFrame*
 ExecuteState::pushInterpreterFrame(JSContext* cx)
 {
     return cx->runtime()->interpreterStack().pushExecuteFrame(cx, script_, thisv_, scopeChain_,
                                                               type_, evalInFrame_);
 }
+namespace js {
+
+// Implementation of per-performance group performance measurement.
+//
+//
+// All mutable state is stored in `Runtime::stopwatch` (per-process
+// performance stats and logistics) and in `PerformanceGroup` (per
+// group performance stats).
+struct AutoStopwatch final
+{
+    // If the stopwatch is active, constructing an instance of
+    // AutoStopwatch causes it to become the current owner of the
+    // stopwatch.
+    //
+    // Previous owner is restored upon destruction.
+    explicit inline AutoStopwatch(JSContext *cx MOZ_GUARD_OBJECT_NOTIFIER_PARAM)
+      : compartment_(nullptr)
+      , runtime_(nullptr)
+      , iteration_(0)
+      , isActive_(false)
+      , isTop_(false)
+      , userTimeStart_(0)
+      , systemTimeStart_(0)
+      , CPOWTimeStart_(0)
+    {
+        MOZ_GUARD_OBJECT_NOTIFIER_INIT;
+        runtime_ = cx->runtime();
+        if (!runtime_->stopwatch.isActive())
+            return;
+        compartment_ = cx->compartment();
+        MOZ_ASSERT(compartment_);
+        if (compartment_->scheduledForDestruction)
+            return;
+        iteration_ = runtime_->stopwatch.iteration;
+
+        PerformanceGroup *group = compartment_->performanceMonitoring.getGroup();
+        MOZ_ASSERT(group);
+
+        if (group->hasStopwatch(iteration_)) {
+            // Someone is already monitoring this group during this
+            // tick, no need for further monitoring.
+            return;
+        }
+
+        // Start the stopwatch.
+        if (!this->getTimes(&userTimeStart_, &systemTimeStart_))
+            return;
+        isActive_ = true;
+        CPOWTimeStart_ = runtime_->stopwatch.performance.totalCPOWTime;
+
+        // We are now in charge of monitoring this group for the tick,
+        // until destruction of `this` or until we enter a nested event
+        // loop and `iteration_` is incremented.
+        group->acquireStopwatch(iteration_, this);
+
+        if (runtime_->stopwatch.isEmpty) {
+            // This is the topmost stopwatch on the stack.
+            // It will be in charge of updating the per-process
+            // performance data.
+            runtime_->stopwatch.isEmpty = false;
+            runtime_->stopwatch.performance.ticks++;
+            isTop_ = true;
+        }
+    }
+    inline ~AutoStopwatch() {
+        if (!isActive_) {
+            // We are not in charge of monitoring anything.
+            return;
+        }
+
+        MOZ_ASSERT(!compartment_->scheduledForDestruction);
+
+        if (!runtime_->stopwatch.isActive()) {
+            // Monitoring has been stopped while we were
+            // executing the code. Drop everything.
+            return;
+        }
+
+        if (iteration_ != runtime_->stopwatch.iteration) {
+            // We have entered a nested event loop at some point.
+            // Any information we may have is obsolete.
+            return;
+        }
+
+        PerformanceGroup *group = compartment_->performanceMonitoring.getGroup();
+        MOZ_ASSERT(group);
+
+        // Compute time spent.
+        group->releaseStopwatch(iteration_, this);
+        uint64_t userTimeEnd, systemTimeEnd;
+        if (!this->getTimes(&userTimeEnd, &systemTimeEnd))
+            return;
+
+        uint64_t userTimeDelta = userTimeEnd - userTimeStart_;
+        uint64_t systemTimeDelta = systemTimeEnd - systemTimeStart_;
+        uint64_t CPOWTimeDelta = runtime_->stopwatch.performance.totalCPOWTime - CPOWTimeStart_;
+        group->data.totalUserTime += userTimeDelta;
+        group->data.totalSystemTime += systemTimeDelta;
+        group->data.totalCPOWTime += CPOWTimeDelta;
+
+        uint64_t totalTimeDelta = userTimeDelta + systemTimeDelta;
+        updateDurations(totalTimeDelta, group->data.durations);
+        group->data.ticks++;
+
+        if (isTop_) {
+            // This is the topmost stopwatch on the stack.
+            // Record the timing information.
+            runtime_->stopwatch.performance.totalUserTime = userTimeEnd;
+            runtime_->stopwatch.performance.totalSystemTime = systemTimeEnd;
+            updateDurations(totalTimeDelta, runtime_->stopwatch.performance.durations);
+            runtime_->stopwatch.isEmpty = true;
+        }
+    }
+
+ private:
+
+    // Update an array containing the number of times we have missed
+    // at least 2^0 successive ms, 2^1 successive ms, ...
+    // 2^i successive ms.
+    template<int N>
+    void updateDurations(uint64_t totalTimeDelta, uint64_t (&array)[N]) const {
+        // Duration of one frame, i.e. 16ms in museconds
+        size_t i = 0;
+        uint64_t duration = 1000;
+        for (i = 0, duration = 1000;
+             i < N && duration < totalTimeDelta;
+             ++i, duration *= 2) {
+            array[i]++;
+        }
+    }
+
+    // Get the OS-reported time spent in userland/systemland,
+    // in microseconds.
+    bool getTimes(uint64_t *userTime, uint64_t *systemTime) const {
+        MOZ_ASSERT(userTime);
+        MOZ_ASSERT(systemTime);
+
+#if defined(XP_UNIX)
+
+        struct rusage rusage;
+#if defined(RUSAGE_THREAD)
+        // Under Linux, we can obtain per-thread statistics
+        int err = getrusage(RUSAGE_THREAD, &rusage);
+#else
+        // Under other Unices, including MacOS X, we need to
+        // do with more noisy per-process statistics.
+        int err = getrusage(RUSAGE_SELF, &rusage);
+#endif // defined(RUSAGE_THREAD)
+        MOZ_ASSERT(!err);
+        if (err)
+            return false;
+
+        *userTime = rusage.ru_utime.tv_usec
+            + rusage.ru_utime.tv_sec * 1000000;
+        *systemTime = rusage.ru_stime.tv_usec
+            + rusage.ru_stime.tv_sec * 1000000;
+
+#elif defined(XP_WIN)
+        // Under Windows, we can obtain per-thread statistics,
+        // although experience seems to suggest that they are
+        // not very good under Windows XP.
+        FILETIME creationFileTime; // Ignored
+        FILETIME exitFileTime; // Ignored
+        FILETIME kernelFileTime;
+        FILETIME userFileTime;
+        BOOL success = GetThreadTimes(GetCurrentThread(),
+                                      &creationFileTime, &exitFileTime,
+                                      &kernelFileTime, &userFileTime);
+        MOZ_ASSERT(success);
+        if (!success)
+            return false;
+
+        ULARGE_INTEGER kernelTimeInt;
+        ULARGE_INTEGER userTimeInt;
+        kernelTimeInt.LowPart = kernelFileTime.dwLowDateTime;
+        kernelTimeInt.HighPart = kernelFileTime.dwHighDateTime;
+        *systemTime = kernelTimeInt.QuadPart / 10; // 100 ns to 1 us
+
+        userTimeInt.LowPart = userFileTime.dwLowDateTime;
+        userTimeInt.HighPart = userFileTime.dwHighDateTime;
+        *userTime = userTimeInt.QuadPart / 10; // 100 ns to 1 us
+#endif // defined(XP_UNIX) || defined(XP_WIN)
+
+        return true;
+    }
+
+  private:
+    // The compartment with which this object was initialized.
+    // Non-null.
+    JSCompartment *compartment_;
+
+    // The runtime with which this object was initialized.
+    // Non-null.
+    JSRuntime *runtime_;
+
+    // An indication of the number of times we have entered the event
+    // loop.  Used only for comparison.
+    uint64_t iteration_;
+
+    // `true` if this object is currently used to monitor performance,
+    // `false` otherwise, i.e. if the stopwatch mechanism is off or if
+    // another stopwatch is already in charge of monitoring for the
+    // same PerformanceGroup.
+    bool isActive_;
+
+    // `true` if this stopwatch is the topmost stopwatch on the stack
+    // for this event, `false` otherwise.
+    bool isTop_;
+
+    // Timestamps captured while starting the stopwatch.
+    uint64_t userTimeStart_;
+    uint64_t systemTimeStart_;
+    uint64_t CPOWTimeStart_;
+
+    MOZ_DECL_USE_GUARD_OBJECT_NOTIFIER
+};
+
+}
 
 bool
 js::RunScript(JSContext* cx, RunState& state)
 {
     JS_CHECK_RECURSION(cx, return false);
 
+#if defined(NIGHTLY_BUILD)
+    js::AutoStopwatch stopwatch(cx);
+#endif // defined(NIGHTLY_BUILD)
+
     SPSEntryMarker marker(cx->runtime(), state.script());
 
     state.script()->ensureNonLazyCanonicalFunction(cx);
 
     if (jit::IsIonEnabled(cx)) {
         jit::MethodStatus status = jit::CanEnter(cx, state);
         if (status == jit::Method_Error)
             return false;
--- a/js/src/vm/MemoryMetrics.cpp
+++ b/js/src/vm/MemoryMetrics.cpp
@@ -790,28 +790,28 @@ JS::CollectRuntimeStats(JSRuntime* rt, R
     return true;
 }
 
 JS_PUBLIC_API(size_t)
 JS::SystemCompartmentCount(JSRuntime* rt)
 {
     size_t n = 0;
     for (CompartmentsIter comp(rt, WithAtoms); !comp.done(); comp.next()) {
-        if (comp->isSystem)
+        if (comp->isSystem())
             ++n;
     }
     return n;
 }
 
 JS_PUBLIC_API(size_t)
 JS::UserCompartmentCount(JSRuntime* rt)
 {
     size_t n = 0;
     for (CompartmentsIter comp(rt, WithAtoms); !comp.done(); comp.next()) {
-        if (!comp->isSystem)
+        if (!comp->isSystem())
             ++n;
     }
     return n;
 }
 
 JS_PUBLIC_API(size_t)
 JS::PeakSizeOfTemporary(const JSRuntime* rt)
 {
--- a/js/src/vm/Runtime.cpp
+++ b/js/src/vm/Runtime.cpp
@@ -285,17 +285,17 @@ JSRuntime::init(uint32_t maxbytes, uint3
     JS::CompartmentOptions options;
     ScopedJSDeletePtr<JSCompartment> atomsCompartment(new_<JSCompartment>(atomsZone.get(), options));
     if (!atomsCompartment || !atomsCompartment->init(nullptr))
         return false;
 
     gc.zones.append(atomsZone.get());
     atomsZone->compartments.append(atomsCompartment.get());
 
-    atomsCompartment->isSystem = true;
+    atomsCompartment->setIsSystem(true);
 
     atomsZone.forget();
     this->atomsCompartment_ = atomsCompartment.forget();
 
     if (!symbolRegistry_.init())
         return false;
 
     if (!scriptDataTable_.init())
@@ -854,8 +854,98 @@ JS::UpdateJSRuntimeProfilerSampleBufferG
 }
 
 JS_FRIEND_API(bool)
 JS::IsProfilingEnabledForRuntime(JSRuntime* runtime)
 {
     MOZ_ASSERT(runtime);
     return runtime->spsProfiler.enabled();
 }
+
+void
+js::ResetStopwatches(JSRuntime *rt)
+{
+    MOZ_ASSERT(rt);
+    rt->stopwatch.reset();
+}
+
+bool
+js::SetStopwatchActive(JSRuntime *rt, bool isActive)
+{
+    MOZ_ASSERT(rt);
+    return rt->stopwatch.setIsActive(isActive);
+}
+
+bool
+js::IsStopwatchActive(JSRuntime *rt)
+{
+    MOZ_ASSERT(rt);
+    return rt->stopwatch.isActive();
+}
+
+js::PerformanceGroupHolder::~PerformanceGroupHolder()
+{
+    unlink();
+}
+
+void*
+js::PerformanceGroupHolder::getHashKey()
+{
+    return compartment_->isSystem() ?
+        (void*)compartment_->addonId :
+        (void*)JS_GetCompartmentPrincipals(compartment_);
+    // This key may be `nullptr` if we have `isSystem() == true`
+    // and `compartment_->addonId`. This is absolutely correct,
+    // and this represents the `PerformanceGroup` used to track
+    // the performance of the the platform compartments.
+}
+
+void
+js::PerformanceGroupHolder::unlink()
+{
+    if (!group_) {
+        // The group has never been instantiated.
+        return;
+    }
+
+    js::PerformanceGroup* group = group_;
+    group_ = nullptr;
+
+    if (group->decRefCount() > 0) {
+        // The group has at least another owner.
+        return;
+    }
+
+
+    JSRuntime::Stopwatch::Groups::Ptr ptr =
+        runtime_->stopwatch.groups_.lookup(getHashKey());
+    MOZ_ASSERT(ptr);
+    runtime_->stopwatch.groups_.remove(ptr);
+    js_delete(group);
+}
+
+PerformanceGroup *
+js::PerformanceGroupHolder::getGroup()
+{
+    if (group_)
+        return group_;
+
+    void* key = getHashKey();
+    JSRuntime::Stopwatch::Groups::AddPtr ptr =
+        runtime_->stopwatch.groups_.lookupForAdd(key);
+    if (ptr) {
+        group_ = ptr->value();
+        MOZ_ASSERT(group_);
+    } else {
+        group_ = runtime_->new_<PerformanceGroup>();
+        runtime_->stopwatch.groups_.add(ptr, key, group_);
+    }
+
+    group_->incRefCount();
+
+    return group_;
+}
+
+PerformanceData*
+js::GetPerformanceData(JSRuntime *rt)
+{
+    return &rt->stopwatch.performance;
+}
--- a/js/src/vm/Runtime.h
+++ b/js/src/vm/Runtime.h
@@ -562,16 +562,17 @@ class PerThreadData : public PerThreadDa
 
 #if defined(JS_ARM_SIMULATOR) || defined(JS_MIPS_SIMULATOR)
     js::jit::Simulator* simulator() const;
 #endif
 };
 
 class AutoLockForExclusiveAccess;
 
+struct AutoStopwatch;
 } // namespace js
 
 struct JSRuntime : public JS::shadow::Runtime,
                    public js::MallocProvider<JSRuntime>
 {
     /*
      * Per-thread data for the main thread that is associated with
      * this JSRuntime, as opposed to any worker threads used in
@@ -1448,16 +1449,123 @@ struct JSRuntime : public JS::shadow::Ru
     /*
      * Debugger.Memory functions like takeCensus use this embedding-provided
      * function to assess the size of malloc'd blocks of memory.
      */
     mozilla::MallocSizeOf debuggerMallocSizeOf;
 
     /* Last time at which an animation was played for this runtime. */
     int64_t lastAnimationTime;
+
+  public:
+
+    /* ------------------------------------------
+       Performance measurements
+       ------------------------------------------ */
+    struct Stopwatch {
+        /**
+         * The number of times we have entered the event loop.
+         * Used to reset counters whenever we enter the loop,
+         * which may be caused either by having completed the
+         * previous run of the event loop, or by entering a
+         * nested loop.
+         *
+         * Always incremented by 1, may safely overflow.
+         */
+        uint64_t iteration;
+
+        /**
+         * `true` if no stopwatch has been registered for the
+         * current run of the event loop, `false` until then.
+         */
+        bool isEmpty;
+
+        /**
+         * Performance data on the entire runtime.
+         */
+        js::PerformanceData performance;
+
+        Stopwatch()
+          : iteration(0)
+          , isEmpty(true)
+          , isActive_(false)
+        { }
+
+        /**
+         * Reset the stopwatch.
+         *
+         * This method is meant to be called whenever we start processing
+         * an event, to ensure that stop any ongoing measurement that would
+         * otherwise provide irrelevant results.
+         */
+        void reset() {
+            ++iteration;
+            isEmpty = true;
+        }
+
+        /**
+         * Activate/deactivate stopwatch measurement.
+         *
+         * Noop if `value` is `true` and the stopwatch is already active,
+         * or if `value` is `false` and the stopwatch is already inactive.
+         *
+         * Otherwise, any pending measurements are dropped, but previous
+         * measurements remain stored.
+         *
+         * May return `false` if the underlying hashtable cannot be allocated.
+         */
+        bool setIsActive(bool value) {
+            if (isActive_ != value)
+                reset();
+
+            if (value && !groups_.initialized()) {
+                if (!groups_.init(128))
+                    return false;
+            }
+
+            isActive_ = value;
+            return true;
+        }
+
+        /**
+         * `true` if the stopwatch is currently monitoring, `false` otherwise.
+         */
+        bool isActive() const {
+            return isActive_;
+        }
+
+    private:
+        /**
+         * A map used to collapse compartments belonging to the same
+         * add-on (respectively to the same webpage, to the platform)
+         * into a single group.
+         *
+         * Keys: for system compartments, a `JSAddonId*` (which may be
+         * `nullptr`), and for webpages, a `JSPrincipals*` (which may
+         * not). Note that compartments may start as non-system
+         * compartments and become compartments later during their
+         * lifetime, which requires an invalidation.
+         *
+         * This map is meant to be accessed only by instances of
+         * PerformanceGroupHolder, which handle both reference-counting
+         * of the values and invalidation of the key/value pairs.
+         */
+        typedef js::HashMap<void*, js::PerformanceGroup*,
+                            js::DefaultHasher<void*>,
+                            js::SystemAllocPolicy> Groups;
+
+        Groups groups_;
+        friend struct js::PerformanceGroupHolder;
+
+        /**
+         * `true` if stopwatch monitoring is active, `false` otherwise.
+         */
+        bool isActive_;
+    };
+    Stopwatch stopwatch;
 };
 
 namespace js {
 
 // When entering JIT code, the calling JSContext* is stored into the thread's
 // PerThreadData. This function retrieves the JSContext with the pre-condition
 // that the caller is JIT code or C++ called directly from JIT code. This
 // function should not be called from arbitrary locations since the JSContext
--- a/js/src/vm/SavedStacks.cpp
+++ b/js/src/vm/SavedStacks.cpp
@@ -365,17 +365,17 @@ static SavedFrame*
 GetFirstSubsumedFrame(JSContext* cx, HandleSavedFrame frame, bool& skippedAsync)
 {
     skippedAsync = false;
 
     JSSubsumesOp subsumes = cx->runtime()->securityCallbacks->subsumes;
     if (!subsumes)
         return frame;
 
-    JSPrincipals* principals = cx->compartment()->principals;
+    JSPrincipals* principals = cx->compartment()->principals();
 
     RootedSavedFrame rootedFrame(cx, frame);
     while (rootedFrame && !subsumes(principals, rootedFrame->getPrincipals())) {
         if (rootedFrame->getAsyncCause())
             skippedAsync = true;
         rootedFrame = rootedFrame->getParent();
     }
 
@@ -470,18 +470,18 @@ public:
                                    MOZ_GUARD_OBJECT_NOTIFIER_PARAM)
     {
         MOZ_GUARD_OBJECT_NOTIFIER_INIT;
         // Note that obj might be null here, since we're doing this
         // before UnwrapSavedFrame.
         if (obj && cx->compartment() != obj->compartment())
         {
             JSSubsumesOp subsumes = cx->runtime()->securityCallbacks->subsumes;
-            if (subsumes && subsumes(cx->compartment()->principals,
-                                     obj->compartment()->principals))
+            if (subsumes && subsumes(cx->compartment()->principals(),
+                                     obj->compartment()->principals()))
             {
                 ac_.emplace(cx, obj);
             }
         }
     }
 
  private:
     Maybe<JSAutoCompartment> ac_;
@@ -644,17 +644,17 @@ BuildStackString(JSContext* cx, HandleOb
         bool skippedAsync;
         js::RootedSavedFrame frame(cx, UnwrapSavedFrame(cx, stack, skippedAsync));
         if (!frame) {
             stringp.set(cx->runtime()->emptyString);
             return true;
         }
 
         DebugOnly<JSSubsumesOp> subsumes = cx->runtime()->securityCallbacks->subsumes;
-        DebugOnly<JSPrincipals*> principals = cx->compartment()->principals;
+        DebugOnly<JSPrincipals*> principals = cx->compartment()->principals();
 
         js::RootedSavedFrame parent(cx);
         do {
             MOZ_ASSERT_IF(subsumes, (*subsumes)(principals, frame->getPrincipals()));
 
             if (!frame->isSelfHosted()) {
                 RootedString asyncCause(cx, frame->getAsyncCause());
                 if (!asyncCause && skippedAsync)
@@ -931,17 +931,17 @@ SavedStacks::insertFrames(JSContext* cx,
         }
         new (&stackChain->back()) SavedFrame::Lookup(
           location->source,
           location->line,
           location->column,
           iter.isNonEvalFunctionFrame() ? iter.functionDisplayAtom() : nullptr,
           nullptr,
           nullptr,
-          iter.compartment()->principals
+          iter.compartment()->principals()
         );
 
         ++iter;
 
         // If maxFrameCount is zero there's no limit on the number of frames.
         if (maxFrameCount == 0)
             continue;
 
--- a/js/src/vm/SelfHosting.cpp
+++ b/js/src/vm/SelfHosting.cpp
@@ -1128,17 +1128,17 @@ JSRuntime::createSelfHostingGlobal(JSCon
 
     AutoCompartment ac(cx, compartment);
     Rooted<GlobalObject*> shg(cx, GlobalObject::createInternal(cx, &shgClass));
     if (!shg)
         return nullptr;
 
     cx->runtime()->selfHostingGlobal_ = shg;
     compartment->isSelfHosting = true;
-    compartment->isSystem = true;
+    compartment->setIsSystem(true);
 
     if (!GlobalObject::initSelfHostingBuiltins(cx, shg, intrinsic_functions))
         return nullptr;
 
     JS_FireOnNewGlobalObject(cx, shg);
 
     return shg;
 }
--- a/js/src/vm/Stack-inl.h
+++ b/js/src/vm/Stack-inl.h
@@ -247,17 +247,17 @@ InterpreterStack::purge(JSRuntime* rt)
 {
     rt->gc.freeUnusedLifoBlocksAfterSweeping(&allocator_);
 }
 
 uint8_t*
 InterpreterStack::allocateFrame(JSContext* cx, size_t size)
 {
     size_t maxFrames;
-    if (cx->compartment()->principals == cx->runtime()->trustedPrincipals())
+    if (cx->compartment()->principals() == cx->runtime()->trustedPrincipals())
         maxFrames = MAX_FRAMES_TRUSTED;
     else
         maxFrames = MAX_FRAMES;
 
     if (MOZ_UNLIKELY(frameCount_ >= maxFrames)) {
         ReportOverRecursed(cx);
         return nullptr;
     }
--- a/js/src/vm/Stack.cpp
+++ b/js/src/vm/Stack.cpp
@@ -525,17 +525,17 @@ FrameIter::settleOnActivation()
             continue;
         }
 
         // If the caller supplied principals, only show activations which are subsumed (of the same
         // origin or of an origin accessible) by these principals.
         if (data_.principals_) {
             JSContext* cx = data_.cx_->asJSContext();
             if (JSSubsumesOp subsumes = cx->runtime()->securityCallbacks->subsumes) {
-                if (!subsumes(data_.principals_, activation->compartment()->principals)) {
+                if (!subsumes(data_.principals_, activation->compartment()->principals())) {
                     ++data_.activations_;
                     continue;
                 }
             }
         }
 
         if (activation->isJit()) {
             data_.jitFrames_ = jit::JitFrameIterator(data_.activations_);
--- a/js/xpconnect/src/nsXPConnect.cpp
+++ b/js/xpconnect/src/nsXPConnect.cpp
@@ -343,23 +343,16 @@ xpc::TraceXPCGlobal(JSTracer* trc, JSObj
     xpc::CompartmentPrivate* compartmentPrivate = xpc::CompartmentPrivate::Get(obj);
     if (compartmentPrivate && compartmentPrivate->scope)
         compartmentPrivate->scope->TraceInside(trc);
 }
 
 
 namespace xpc {
 
-uint64_t
-GetCompartmentCPOWMicroseconds(JSCompartment* compartment)
-{
-    xpc::CompartmentPrivate* compartmentPrivate = xpc::CompartmentPrivate::Get(compartment);
-    return compartmentPrivate ? PR_IntervalToMicroseconds(compartmentPrivate->CPOWTime) : 0;
-}
-
 JSObject*
 CreateGlobalObject(JSContext* cx, const JSClass* clasp, nsIPrincipal* principal,
                    JS::CompartmentOptions& aOptions)
 {
     MOZ_ASSERT(NS_IsMainThread(), "using a principal off the main thread?");
     MOZ_ASSERT(principal);
 
     MOZ_RELEASE_ASSERT(principal != nsContentUtils::GetNullSubjectPrincipal(),
--- a/js/xpconnect/src/xpcprivate.h
+++ b/js/xpconnect/src/xpcprivate.h
@@ -622,16 +622,17 @@ public:
     void InitSingletonScopes();
     void DeleteSingletonScopes();
 
     PRTime GetWatchdogTimestamp(WatchdogTimestampCategory aCategory);
 
     void OnProcessNextEvent() {
         mSlowScriptCheckpoint = mozilla::TimeStamp::NowLoRes();
         mSlowScriptSecondHalf = false;
+        js::ResetStopwatches(Get()->Runtime());
     }
     void OnAfterProcessNextEvent() {
         mSlowScriptCheckpoint = mozilla::TimeStamp();
         mSlowScriptSecondHalf = false;
     }
 
     nsTArray<nsXPCWrappedJS*>& WrappedJSToReleaseArray() { return mWrappedJSToReleaseArray; }
 
@@ -3642,17 +3643,16 @@ public:
     };
 
     explicit CompartmentPrivate(JSCompartment* c)
         : wantXrays(false)
         , writeToGlobalPrototype(false)
         , skipWriteToGlobalPrototype(false)
         , universalXPConnectEnabled(false)
         , forcePermissiveCOWs(false)
-        , CPOWTime(0)
         , skipCOWCallableChecks(false)
         , scriptability(c)
         , scope(nullptr)
     {
         MOZ_COUNT_CTOR(xpc::CompartmentPrivate);
         mozilla::PodArrayZero(wrapperDenialWarnings);
     }
 
@@ -3696,19 +3696,16 @@ public:
     // This is only ever set during mochitest runs when enablePrivilege is called.
     // It allows the SpecialPowers scope to waive the normal chrome security
     // wrappers and expose properties directly to content. This lets us avoid a
     // bunch of overhead and complexity in our SpecialPowers automation glue.
     //
     // Using it in production is inherently unsafe.
     bool forcePermissiveCOWs;
 
-    // A running count of how much time we've spent processing CPOWs.
-    PRIntervalTime               CPOWTime;
-
     // Disables the XPConnect security checks that deny access to callables and
     // accessor descriptors on COWs. Do not use this unless you are bholley.
     bool skipCOWCallableChecks;
 
     // Whether we've emitted a warning about a property that was filtered out
     // by a security wrapper. See XrayWrapper.cpp.
     bool wrapperDenialWarnings[WrapperDenialTypeCount];