Bug 1369955: Implement a JavaScript `Measure` API to the perfstream::Measure tree. draft
authorJim Blandy <jimb@mozilla.com>
Fri, 23 Jun 2017 13:43:56 -0700
changeset 684581 49b733ffdcb6e15f338ee212cd964671c4e32931
parent 684580 23f51596ac386586bbe7505f82f671143edee43c
child 684582 ff4171113cff695ee50ab5a8972956ffdf174a31
push id85652
push userbmo:jimb@mozilla.com
push dateMon, 23 Oct 2017 05:02:44 +0000
bugs1369955
milestone58.0a1
Bug 1369955: Implement a JavaScript `Measure` API to the perfstream::Measure tree. This patch introduces `Measure` and `MeasureGroup` JavaScript constructors, which provide a scripting API to the perfstream Measure tree. These constructors are included in SpiderMonkey shell globals. Tests in jit-test exercise the toy Measures defined in js/src/vm/Measures.cpp. MozReview-Commit-ID: AtXDyFWFtyz
js/public/Measure.h
js/src/builtin/MeasureObject.cpp
js/src/builtin/MeasureObject.h
js/src/jit-test/tests/measures/Measure.js
js/src/jit-test/tests/measures/MeasureGroup.js
js/src/jit-test/tests/measures/attributes.js
js/src/jit-test/tests/measures/switch-proto.js
js/src/js.msg
js/src/moz.build
js/src/shell/js.cpp
js/src/shell/moz.build
--- a/js/public/Measure.h
+++ b/js/public/Measure.h
@@ -27,11 +27,20 @@ extern JS_PUBLIC_API(mozilla::perfstream
 GetMeasures(JSContext*);
 
 /**
  * Like GetMeasures, but include testing-only measures.
  */
 extern JS_PUBLIC_API(mozilla::perfstream::GroupPtr)
 GetMeasuresWithTests(JSContext*);
 
+/**
+ * Add the |Measure| and |MeasureGroup| constructors to |global|, using |root|
+ * as the root of the measure tree. These are JavaScript-level interfaces to the
+ * |mozilla::perfstream::Measure| API.
+ */
+extern MOZ_MUST_USE JS_PUBLIC_API(bool)
+DefineMeasureConstructors(JSContext* cx, JS::HandleObject global,
+                          mozilla::perfstream::GroupPtr&& root);
+
 } // namespace JS
 
 #endif /* js_Measure_h */
new file mode 100644
--- /dev/null
+++ b/js/src/builtin/MeasureObject.cpp
@@ -0,0 +1,805 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 4 -*-
+ * vim: set ts=8 sts=4 et sw=4 tw=99:
+ * 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 "builtin/MeasureObject.h"
+
+#include "mozilla/Maybe.h"
+#include "mozilla/Move.h"
+#include "mozilla/perfstream/Measure.h"
+#include "mozilla/UniquePtr.h"
+#include "mozilla/UniquePtrExtensions.h"
+#include "mozilla/Vector.h"
+
+#include <string.h>
+
+#include "jsapi.h"
+#include "jsarray.h"
+#include "jscntxt.h"
+#include "jsprf.h"
+#include "jsstr.h"
+
+#include "gc/Heap.h"
+#include "gc/Rooting.h"
+#include "js/CharacterEncoding.h"
+#include "js/Class.h"
+#include "js/Measure.h"
+#include "js/Utility.h"
+#include "js/Vector.h"
+#include "vm/GlobalObject.h"
+
+#include "jscntxtinlines.h"
+#include "jsobjinlines.h"
+
+using mozilla::Maybe;
+using mozilla::perfstream::Description;
+using mozilla::perfstream::Group;
+using mozilla::perfstream::GroupPtr;
+using mozilla::perfstream::GroupVector;
+using mozilla::perfstream::Measure;
+using mozilla::perfstream::MeasurePtr;
+using mozilla::perfstream::MeasureVector;
+using mozilla::Nothing;
+using mozilla::Some;
+using mozilla::UniqueFreePtr;
+
+namespace js {
+
+
+/* Shared utility functions. */
+
+static UniqueChars
+ToUTF8(JSContext* cx, HandleValue v)
+{
+    RootedString str(cx, ToString<CanGC>(cx, v));
+    if (!str)
+        return nullptr;
+    return UniqueChars(JS_EncodeStringToUTF8(cx, str));
+}
+
+static bool
+UTF8ToString(JSContext* cx, const char* utf8, MutableHandleValue result)
+{
+    JS::ConstUTF8CharsZ chars(utf8, strlen(utf8));
+    RootedString string(cx, JS_NewStringCopyUTF8Z(cx, chars));
+    if (!string)
+        return false;
+
+    result.setString(string);
+    return true;
+}
+
+static bool
+ReportNoPath(JSContext* cx, HandleValue path)
+{
+    RootedString source(cx, ValueToSource(cx, path));
+    if (!source)
+        return false;
+    UniqueChars utf8(JS_EncodeStringToUTF8(cx, source));
+    if (!source)
+        return false;
+    JS_ReportErrorNumberUTF8(cx, GetErrorMessage, nullptr,
+                             JSMSG_MEASURE_NO_PATH, utf8.get());
+    return false;
+}
+
+/*
+ * Check the type of a path argument to a Measure or MeasureGroup constructor,
+ * retrieve its elements, and convert them to strings. On success, return a
+ * vector of null-terminated UTF-8 strings.
+ *
+ * On failure, report the problem on cx and return Nothing().
+ */
+static Maybe<js::Vector<UniqueChars>>
+GetPathElementsAsUTF8(JSContext* cx, HandleValue path)
+{
+    if (!path.isObject() || !path.toObject().is<ArrayObject>()) {
+        ReportValueErrorFlags(cx, JSREPORT_ERROR, JSMSG_UNEXPECTED_TYPE,
+                              JSDVG_SEARCH_STACK, path, nullptr, "not an array",
+                              nullptr);
+        return Nothing();
+    }
+
+    // Get the array's elements as a vector of Values.
+    RootedArrayObject pathArray(cx, &path.toObject().as<ArrayObject>());
+    uint32_t length;
+    if (!GetLengthProperty(cx, pathArray, &length))
+        return Nothing();
+
+    Rooted<ValueVector> pathValues(cx, ValueVector(cx));
+    if (!pathValues.resize(length) || !GetElements(cx, pathArray, length, pathValues.begin()))
+        return Nothing();
+
+    js::Vector<UniqueChars> pathUTF8(cx);
+    if (!pathUTF8.reserve(pathValues.length()))
+        return Nothing();
+
+    for (size_t i = 0; i < pathValues.length(); i++) {
+        UniqueChars utf8 = ToUTF8(cx, pathValues[i]);
+        if (!utf8)
+            return Nothing();
+        pathUTF8.infallibleAppend(Move(utf8));
+    }
+
+    return Some(Move(pathUTF8));
+}
+
+/*
+ * Follow the first length elements of path from start and return the group
+ * there. pathValue is the JavaScript representation of the path, for use in
+ * error messages.
+ *
+ * If there is no such group, or some other sort of error arises, return nullptr
+ * and report an error on cx.
+ */
+static GroupPtr
+FollowGroupPath(JSContext* cx, const Group& start,
+                Vector<UniqueChars>& path, size_t length,
+                HandleValue pathValue)
+{
+    GroupPtr here = start.Clone();
+
+    // Note that length may not be the entire path, so using C++ iteration isn't
+    // appropriate here.
+    for (size_t i = 0; i < length; i++) {
+        auto next = here->GetSubgroup(path[i].get());
+        if (!next) {
+            ReportNoPath(cx, pathValue);
+            return nullptr;
+        }
+
+        here = Move(*next);
+    }
+
+    return here;
+}
+
+static bool
+GetMetadata(JSContext* cx, const Description& description, HandleValue name, MutableHandleValue result)
+{
+    RootedString nameString(cx, ToString<CanGC>(cx, name));
+    if (!nameString)
+        return false;
+    UniqueChars nameUtf8(JS_EncodeStringToUTF8(cx, nameString));
+    if (!nameUtf8)
+        return false;
+    size_t nameLength = strlen(nameUtf8.get());
+
+    for (const char* const* entry = description.mMetadata; *entry; entry++) {
+        if (strncmp(*entry, nameUtf8.get(), nameLength) == 0 &&
+            strncmp(*entry + nameLength, ": ", 2) == 0)
+        {
+            const char* value = *entry + nameLength + 2;
+            return UTF8ToString(cx, value, result);
+        }
+    }
+
+    result.setUndefined();
+    return true;
+}
+
+static bool
+CommonToString(JSContext* cx, const char* className, const Description& description,
+               MutableHandleValue result)
+{
+    UniqueChars cString = JS_smprintf("[object %s %s]", className, description.mIdentifier);
+    if (!cString)
+        return false;
+    return UTF8ToString(cx, cString.get(), result);
+}
+
+// Generic |this| and argument checking for MeasureObject, MeasureGroupObject.
+template<typename ThisObject>
+class MeasureArgs MOZ_STACK_CLASS {
+  public:
+    MeasureArgs(JSContext* cx, CallArgs args, const char *fnName)
+      : cx_(cx),
+        args(args),
+        fnName(fnName),
+        object_(cx),
+        prototype_(cx)
+    { }
+
+    // Check that JS call's |this| value is a usable instance, and not a
+    // prototype. Otherwise, throw a JavaScript exception on |cx_|.
+    bool check() { return checkThis(); }
+
+    // As with |check()|, but throw if we have not been passed `n` arguments.
+    bool requireAtLeast(size_t n) {
+        return checkThis() && args.requireAtLeast(cx_, fnName, n);
+    }
+
+    // Return the JavaScript |this| object, properly typed.
+    ThisObject& object() const { return *object_; }
+
+    // Return the JavaScript |this| object's prototype.
+    Handle<typename ThisObject::Prototype*> prototype() const { return prototype_; }
+
+    JSContext* cx() const { return cx_; }
+
+    // Pass through some methods from our CallArgs member.
+    MutableHandleValue operator[](unsigned i) const { return args[i]; }
+    MutableHandleValue rval() const { return args.rval(); }
+
+  private:
+    JSContext* cx_;
+    CallArgs args;
+    const char* fnName;
+    Rooted<ThisObject*> object_;
+    Rooted<typename ThisObject::Prototype*> prototype_;
+
+    // Check that args.this() is a usable ThisObject instance, and not some other
+    // kind of value or prototype. On failure, return false and report an error
+    // on cx_.
+    MOZ_MUST_USE bool checkThis() {
+        const Value& thisValue = args.thisv();
+
+        if (!thisValue.isObject()) {
+            JS_ReportErrorNumberASCII(cx_, GetErrorMessage, nullptr, JSMSG_NOT_NONNULL_OBJECT,
+                                      InformalValueTypeName(thisValue));
+            return false;
+        }
+
+        JSObject& thisObject = thisValue.toObject();
+        if (!thisObject.is<ThisObject>()) {
+            JS_ReportErrorNumberASCII(cx_, GetErrorMessage, nullptr, JSMSG_INCOMPATIBLE_PROTO,
+                                      ThisObject::class_.name, fnName,
+                                      thisObject.getClass()->name);
+            return false;
+        }
+
+        object_ = &thisObject.as<ThisObject>();
+
+        // ThisObject is never a proxy class, so this should always succeed.
+        RootedObject untypedProto(cx_);
+        if (!GetPrototype(cx_, object_, &untypedProto))
+            MOZ_CRASH("Getting prototype of Measure or MeasureGroup should never fail");
+        if (!untypedProto->is<typename ThisObject::Prototype>()) {
+            ReportValueError(cx_, JSMSG_BAD_PROTOTYPE_TYPE, -1, args.thisv(), nullptr);
+            return false;
+        }
+        prototype_ = &untypedProto->as<typename ThisObject::Prototype>();
+
+        return true;
+    }
+};
+
+
+/* The Measure.prototype object. */
+
+const ClassOps MeasurePrototypeObject::classOps = {
+    nullptr, // addProperty
+    nullptr, // delProperty
+    nullptr, // enumerate
+    nullptr, // newEnumerate
+    nullptr, // resolve
+    nullptr, // mayResolve
+    finalize,
+};
+
+const Class MeasurePrototypeObject::class_ = {
+    "MeasurePrototype",
+    JSCLASS_HAS_RESERVED_SLOTS(SlotCount) |
+    JSCLASS_FOREGROUND_FINALIZE,
+    &classOps
+};
+
+/* static */ MeasurePrototypeObject*
+MeasurePrototypeObject::create(JSContext* cx, Handle<GlobalObject*> global, GroupPtr&& root)
+{
+    Rooted<NativeObject*> obj(cx, GlobalObject::createBlankPrototype(cx, global, &class_));
+    if (!obj)
+        return nullptr;
+
+    // No fallible operations permitted beyond this point, to make sure we won't
+    // leak root, and that our finalize operation sees only fully-initialized
+    // instances.
+    Rooted<MeasurePrototypeObject*> proto(cx, &obj->as<MeasurePrototypeObject>());
+    RootedValue rootPrivate(cx, PrivateValue(root.release()));
+    proto->initReservedSlot(RootGroupSlot, rootPrivate);
+
+    return proto;
+}
+
+/* static */ void
+MeasurePrototypeObject::finalize(FreeOp* fop, JSObject* obj)
+{
+    MOZ_ASSERT(fop->onActiveCooperatingThread());
+
+    // To make sure that we free the memory with the same allocator used to
+    // create the Group in the first place, free group by creating a GroupPtr
+    // and then dropping it.
+    Group& group = obj->as<MeasurePrototypeObject>().rootGroup();
+    GroupPtr unique(&group);
+}
+
+
+/* The MeasureGroup object. */
+
+const ClassSpec MeasureGroupObject::classSpec = {
+    GenericCreateConstructor<construct, 0, gc::AllocKind::FUNCTION>,
+    GenericCreatePrototype,
+    nullptr,
+    nullptr,
+    methods,
+    properties,
+};
+
+const ClassOps MeasureGroupObject::classOps = {
+    nullptr, // addProperty
+    nullptr, // delProperty
+    nullptr, // enumerate
+    nullptr, // newEnumerate
+    nullptr, // resolve
+    nullptr, // mayResolve
+    finalize,
+};
+
+const Class MeasureGroupObject::class_ = {
+    "MeasureGroup",
+    JSCLASS_HAS_RESERVED_SLOTS(SlotCount) |
+    JSCLASS_FOREGROUND_FINALIZE,
+    &classOps,
+    &classSpec
+};
+
+const JSPropertySpec MeasureGroupObject::properties[] = {
+    JS_PSG("identifier", identifierGetter, 0),
+    JS_PS_END
+};
+
+const JSFunctionSpec MeasureGroupObject::methods[] = {
+    JS_FN("metadata",  metadataMethod,  0, 0),
+    JS_FN("measures",  measuresMethod,  0, 0),
+    JS_FN("subgroups", subgroupsMethod, 0, 0),
+    JS_FN(js_toString_str,  toStringMethod,  0, 0),
+    JS_FS_END
+};
+
+/* static */ MeasureGroupObject*
+MeasureGroupObject::create(JSContext* cx,
+                           Handle<MeasureGroupObject*> groupProto,
+                           Handle<MeasurePrototypeObject*> measureProto,
+                           mozilla::perfstream::GroupPtr&& group)
+{
+    assertSameCompartment(cx, groupProto, measureProto);
+
+    JSObject *obj = NewObjectWithGivenProto(cx, &class_, groupProto);
+    if (!obj)
+        return nullptr;
+
+    // No fallible operations permitted beyond this point, lest we leak group or
+    // allow our finalize method to see an incompletely initialized instance.
+    Rooted<MeasureGroupObject*> groupObj(cx, &obj->as<MeasureGroupObject>());
+    groupObj->initReservedSlot(GroupSlot, PrivateValue(group.release()));
+    groupObj->initReservedSlot(MeasureProtoSlot, ObjectValue(*measureProto));
+
+    return groupObj;
+}
+
+/* static */ void
+MeasureGroupObject::finalize(FreeOp* fop, JSObject* obj)
+{
+    MOZ_ASSERT(fop->onActiveCooperatingThread());
+
+    // To make sure that we free the memory with the same allocator used to
+    // create the Group in the first place, free group by creating a GroupPtr
+    // and then dropping it.
+    Group& group = obj->as<MeasureGroupObject>().measureGroup();
+    GroupPtr unique(&group);
+}
+
+/* static */ bool
+MeasureGroupObject::DeclareConstructor(JSContext* cx,
+                                       Handle<GlobalObject*> global,
+                                       Handle<MeasurePrototypeObject*> measureProto,
+                                       mozilla::perfstream::GroupPtr&& root)
+{
+    // Find Object.prototype, to serve as MeasureGroup.prototype's prototype.
+    RootedNativeObject objProto(cx, GlobalObject::getOrCreateObjectPrototype(cx, global));
+    if (!objProto)
+        return false;
+
+    // Create the prototype and constructor, link them together, define them on
+    // the global, and so on.
+    RootedNativeObject groupProtoObj(cx, InitClass(cx, global, objProto,
+                                                   &class_, construct, 1, properties, methods,
+                                                   nullptr, nullptr));
+    if (!groupProtoObj)
+        return false;
+
+    // No fallible operations permitted beyond this point, to ensure our
+    // finalize method sees only fully initialized instances, and to avoid
+    // leaking root.
+    Rooted<MeasureGroupObject*> groupProto(cx, &groupProtoObj->as<MeasureGroupObject>());
+    groupProto->initReservedSlot(GroupSlot, PrivateValue(root.release()));
+    groupProto->initReservedSlot(MeasureProtoSlot, ObjectValue(*measureProto));
+
+    return true;
+}
+
+/*
+ * With no arguments, the MeasureGroup constructor returns a MeasureGroup
+ * referring to the root of the measure tree. Passed an array of strings, the
+ * constructor treats that as a path from the root down to some group, and
+ * returns a MeasureGroupObject representing that.
+ */
+/* static */ bool
+MeasureGroupObject::construct(JSContext* cx, unsigned argc, Value* vp)
+{
+    CallArgs args = CallArgsFromVp(argc, vp);
+
+    // Retrieve the root Group for the lookup from MeasureGroup.prototype.
+    RootedObject callee(cx, &args.callee());
+    RootedValue protoVal(cx);
+    if (!GetProperty(cx, callee, callee, cx->names().prototype, &protoVal))
+        return false;
+    Rooted<MeasureGroupObject*> groupProto(cx, &protoVal.toObject().as<MeasureGroupObject>());
+    Rooted<MeasurePrototypeObject*> measureProto(cx, &groupProto->measureProto());
+    const Group& rootGroup = groupProto->measureGroup();
+
+    GroupPtr group;
+
+    // The no-argument case: construct a MeasureGroupObject referring to the
+    // root of the measure tree.
+    if (args.get(0).isUndefined()) {
+        group = rootGroup.Clone();
+    } else {
+        // The one-argument case: follow path elements down from the root to some
+        // interior Group.
+        auto maybePath = GetPathElementsAsUTF8(cx, args[0]);
+        if (!maybePath)
+            return false;
+        js::Vector<UniqueChars> pathUTF8(Move(*maybePath));
+
+        group = FollowGroupPath(cx, rootGroup, pathUTF8, pathUTF8.length(), args[0]);
+    }
+
+    if (!group)
+        return false;
+
+    Rooted<MeasureGroupObject*> groupObj(cx);
+    groupObj = MeasureGroupObject::create(cx, groupProto, measureProto, Move(group));
+    if (!groupObj)
+        return false;
+
+    args.rval().setObject(*groupObj);
+    return true;
+}
+
+/* static */ MOZ_MUST_USE bool
+MeasureGroupObject::identifierGetter(JSContext* cx, unsigned argc, Value* vp)
+{
+    MeasureArgs<MeasureGroupObject> args(cx, CallArgsFromVp(argc, vp),
+                                         "identifier getter");
+    if (!args.check())
+        return false;
+
+    Description description = args.object().measureGroup().GetDescription();
+    return UTF8ToString(cx, description.mIdentifier, args.rval());
+}
+
+/* static */ bool
+MeasureGroupObject::metadataMethod(JSContext* cx, unsigned argc, Value* vp)
+{
+    MeasureArgs<MeasureGroupObject> args(cx, CallArgsFromVp(argc, vp),
+                                         "metadata");
+    if (!args.requireAtLeast(1))
+        return false;
+
+    Description description = args.object().measureGroup().GetDescription();
+    return GetMetadata(cx, description, args[0], args.rval());
+}
+
+/* static */ bool
+MeasureGroupObject::measuresMethod(JSContext* cx, unsigned argc, Value* vp)
+{
+    MeasureArgs<MeasureGroupObject> args(cx, CallArgsFromVp(argc, vp),
+                                         "measures");
+    if (!args.check())
+        return false;
+
+    const Group& group = args.object().measureGroup();
+    Maybe<MeasureVector> maybeMeasures = group.GetMeasures();
+    if (!maybeMeasures)
+        return false;
+    MeasureVector measures(Move(*maybeMeasures));
+
+    Rooted<ValueVector> values(cx, ValueVector(cx));
+    if (!values.reserve(measures.length()))
+        return false;
+
+    Rooted<MeasurePrototypeObject*> measureProto(cx, &args.object().measureProto());
+    for (auto& measure : measures) {
+        RootedObject obj(cx, MeasureObject::create(cx, measureProto, Move(measure)));
+        if (!obj)
+            return false;
+
+        values.infallibleAppend(ObjectValue(*obj.get()));
+    }
+
+    RootedObject array(cx, NewDenseCopiedArray(cx, values.length(), values.begin()));
+    if (!array)
+        return false;
+
+    args.rval().setObject(*array);
+    return true;
+}
+
+/* static */ bool
+MeasureGroupObject::subgroupsMethod(JSContext* cx, unsigned argc, Value* vp)
+{
+    MeasureArgs<MeasureGroupObject> args(cx, CallArgsFromVp(argc, vp),
+                                         "subgroups");
+    if (!args.check())
+        return false;
+
+    const Group& group = args.object().measureGroup();
+    Maybe<GroupVector> maybeSubgroups = group.GetSubgroups();
+    if (!maybeSubgroups)
+        return false;
+    GroupVector subgroups(Move(*maybeSubgroups));
+
+    Rooted<ValueVector> values(cx, ValueVector(cx));
+    if (!values.reserve(subgroups.length()))
+        return false;
+
+    Rooted<MeasurePrototypeObject*> measureProto(cx, &args.object().measureProto());
+    for (auto& subgroup : subgroups) {
+        RootedObject obj(cx, MeasureGroupObject::create(cx,
+                                                        args.prototype(),
+                                                        measureProto,
+                                                        Move(subgroup)));
+        if (!obj)
+            return false;
+
+        values.infallibleAppend(ObjectValue(*obj.get()));
+    }
+
+    RootedObject array(cx, NewDenseCopiedArray(cx, values.length(), values.begin()));
+    if (!array)
+        return false;
+
+    args.rval().setObject(*array);
+    return true;
+}
+
+/* static */ bool
+MeasureGroupObject::toStringMethod(JSContext* cx, unsigned argc, Value* vp)
+{
+    MeasureArgs<MeasureGroupObject> args(cx, CallArgsFromVp(argc, vp),
+                                         js_toString_str);
+    if (!args.check())
+        return false;
+
+    Description description = args.object().measureGroup().GetDescription();
+    return CommonToString(cx, "MeasureGroup", description, args.rval());
+}
+
+
+/* The Measure object. */
+
+const ClassOps MeasureObject::classOps = {
+    nullptr, // addProperty
+    nullptr, // delProperty
+    nullptr, // enumerate
+    nullptr, // newEnumerate
+    nullptr, // resolve
+    nullptr, // mayResolve
+    finalize,
+};
+
+const Class MeasureObject::class_ = {
+    "Measure",
+    JSCLASS_HAS_RESERVED_SLOTS(SlotCount) |
+    JSCLASS_FOREGROUND_FINALIZE,
+    &classOps
+};
+
+const JSPropertySpec MeasureObject::properties[] = {
+    JS_PSGS("enabled", enabledGetter, enabledSetter, 0),
+    JS_PSG("identifier", identifierGetter, 0),
+    JS_PS_END
+};
+
+const JSFunctionSpec MeasureObject::methods[] = {
+    JS_FN("metadata",      metadataMethod, 0, 0),
+    JS_FN(js_toString_str, toStringMethod, 0, 0),
+    JS_FS_END
+};
+
+/* static */ MeasureObject*
+MeasureObject::create(JSContext* cx,
+                      HandleObject proto,
+                      MeasurePtr&& measure)
+{
+    assertSameCompartment(cx, proto);
+
+    JSObject *obj = NewObjectWithGivenProto(cx, &class_, proto);
+    if (!obj)
+        return nullptr;
+
+    // No fallible operations beyond this point, to ensure our finalize function
+    // only sees fully-initialized instances.
+    Rooted<MeasureObject*> measureObj(cx, &obj->as<MeasureObject>());
+    measureObj->initReservedSlot(MeasureSlot, PrivateValue(measure.release()));
+
+    return measureObj;
+}
+
+/* static */ void
+MeasureObject::finalize(FreeOp* fop, JSObject* obj)
+{
+    MOZ_ASSERT(fop->onActiveCooperatingThread());
+
+    // To make sure that we free the memory with the same allocator used to
+    // create the Measure in the first place, free measure by creating a
+    // MeasurePtr and then dropping it.
+    Measure& measure = obj->as<MeasureObject>().measure();
+    MeasurePtr unique(&measure);
+}
+
+/* static */ bool
+MeasureObject::DeclareConstructor(JSContext* cx,
+                                  Handle<GlobalObject*> global,
+                                  Handle<MeasurePrototypeObject*> measureProto)
+{
+    // Create the Measure constructor function.
+    RootedAtom atom(cx, Atomize(cx, class_.name, strlen(class_.name)));
+    if (!atom)
+        return false;
+    RootedFunction constructorFun(cx, NewNativeConstructor(cx, construct, 1, atom));
+    if (!constructorFun)
+        return false;
+
+    // Cross-link the constructor and prototype, and add methods and accessors.
+    if (!LinkConstructorAndPrototype(cx, constructorFun, measureProto) ||
+        !DefinePropertiesAndFunctions(cx, measureProto, properties, methods))
+        return false;
+
+    // Define the Measure constructor on the global. This is the sole
+    // externally-visible side effect, so it's last.
+    RootedId id(cx, AtomToId(atom));
+    RootedValue constructorValue(cx, ObjectValue(*constructorFun));
+    return DefineDataProperty(cx, global, id, constructorValue, 0 /* attrs */);
+}
+
+/*
+ * The Measure constructor takes an array of strings, treats that as a path from
+ * the root Group down to some leaf Measure, and constructs a MeasureObject
+ * representing that Measure.
+ */
+/* static */ bool
+MeasureObject::construct(JSContext* cx, unsigned argc, Value* vp)
+{
+    CallArgs args = CallArgsFromVp(argc, vp);
+    if (!args.requireAtLeast(cx, "Measure", 1))
+        return false;
+
+    // Find the root Group for the lookup by asking MeasureGroup.prototype.
+    // Measure.prototype holds a reference to that.
+    RootedObject callee(cx, &args.callee());
+    RootedValue protoVal(cx);
+    if (!GetProperty(cx, callee, callee, cx->names().prototype, &protoVal))
+        return false;
+    Rooted<MeasurePrototypeObject*> proto(cx, &protoVal.toObject().as<MeasurePrototypeObject>());
+
+    auto maybePath = GetPathElementsAsUTF8(cx, args[0]);
+    if (!maybePath)
+        return false;
+    js::Vector<UniqueChars> pathUTF8(Move(*maybePath));
+
+    // Traverse all path components but the last, to get the parent group.
+    GroupPtr parent = FollowGroupPath(cx, proto->rootGroup(),
+                                      pathUTF8, pathUTF8.length() - 1,
+                                      args[0]);
+    if (!parent)
+        return false;
+
+    /* Look up the Measure under the final path component. */
+    Maybe<MeasurePtr> maybeMeasure = parent->GetMeasure(pathUTF8.back().get());
+    if (!maybeMeasure)
+        return ReportNoPath(cx, args[0]);
+
+    RootedObject obj(cx, MeasureObject::create(cx, proto, Move(*maybeMeasure)));
+    if (!obj)
+        return false;
+
+    args.rval().setObject(*obj);
+    return true;
+}
+
+/* static */ bool
+MeasureObject::enabledSetter(JSContext* cx, unsigned argc, Value* vp)
+{
+    MeasureArgs<MeasureObject> args(cx, CallArgsFromVp(argc, vp),
+                                    "enabled setter");
+    if (!args.requireAtLeast(1))
+        return false;
+
+    Measure& measure = args.object().measure();
+    if (ToBoolean(args[0]))
+        measure.Enable();
+    else
+        measure.Disable();
+
+    args.rval().setUndefined();
+    return true;
+}
+
+/* static */ bool
+MeasureObject::enabledGetter(JSContext* cx, unsigned argc, Value* vp)
+{
+    MeasureArgs<MeasureObject> args(cx, CallArgsFromVp(argc, vp),
+                                    "enabled getter");
+    if (!args.check())
+        return false;
+
+    args.rval().setBoolean(args.object().measure().IsEnabled());
+    return true;
+}
+
+/* static */ bool
+MeasureObject::identifierGetter(JSContext* cx, unsigned argc, Value* vp)
+{
+    MeasureArgs<MeasureObject> args(cx, CallArgsFromVp(argc, vp),
+                                    "identifier getter");
+    if (!args.check())
+        return false;
+
+    Description description = args.object().measure().GetDescription();
+    return UTF8ToString(cx, description.mIdentifier, args.rval());
+}
+
+/* static */ bool
+MeasureObject::metadataMethod(JSContext* cx, unsigned argc, Value* vp)
+{
+    MeasureArgs<MeasureObject> args(cx, CallArgsFromVp(argc, vp),
+                                    "metadata");
+    if (!args.requireAtLeast(1))
+        return false;
+
+    Description description = args.object().measure().GetDescription();
+    return GetMetadata(cx, description, args[0], args.rval());
+}
+
+
+/* static */ bool
+MeasureObject::toStringMethod(JSContext* cx, unsigned argc, Value* vp)
+{
+    MeasureArgs<MeasureObject> args(cx, CallArgsFromVp(argc, vp),
+                                    js_toString_str);
+    if (!args.check())
+        return false;
+
+    Description description = args.object().measure().GetDescription();
+    return CommonToString(cx, "Measure", description, args.rval());
+}
+
+} // namespace js
+
+
+/* Public interfaces. */
+
+namespace JS {
+
+extern JS_PUBLIC_API(bool)
+DefineMeasureConstructors(JSContext* cx, HandleObject obj, GroupPtr&& root)
+{
+    Rooted<GlobalObject*> global(cx, &obj->as<GlobalObject>());
+
+    Rooted<MeasurePrototypeObject*> measureProto(cx);
+    measureProto = MeasurePrototypeObject::create(cx, global, root->Clone());
+    if (!measureProto)
+        return false;
+
+    if (!MeasureGroupObject::DeclareConstructor(cx, global, measureProto, root->Clone()) ||
+        !MeasureObject::DeclareConstructor(cx, global, measureProto))
+        return false;
+
+    return true;
+}
+
+} // namespace JS
new file mode 100644
--- /dev/null
+++ b/js/src/builtin/MeasureObject.h
@@ -0,0 +1,177 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 4 -*-
+ * vim: set ts=8 sts=4 et sw=4 tw=99:
+ * 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/. */
+
+/* A JavaScript interface to mozilla::perfstream::Measure and Group. */
+
+#ifndef builtin_MeasureObject_h
+#define builtin_MeasureObject_h
+
+#include "mozilla/Attributes.h"
+#include "mozilla/perfstream/Measure.h"
+#include "mozilla/UniquePtr.h"
+
+#include "jspubtd.h"
+#include "js/Class.h"
+#include "js/Measure.h"
+#include "js/Value.h"
+
+namespace js {
+
+// The class of Measure.prototype.
+class MeasurePrototypeObject : public NativeObject {
+  public:
+    static const Class class_;
+
+    static MeasurePrototypeObject* create(JSContext*,
+                                          Handle<GlobalObject*> global,
+                                          mozilla::perfstream::GroupPtr&& root);
+
+    // C++ utilities.
+
+    // Return the root Group for path lookups.
+    mozilla::perfstream::Group& rootGroup() const {
+        void* group = getReservedSlot(RootGroupSlot).toPrivate();
+        MOZ_ASSERT(group);
+        return *static_cast<mozilla::perfstream::Group*>(group);
+    }
+
+  private:
+    // Reserved JSObject slot indices.
+    enum {
+        // The root Group, for the Measure constructor's path lookups.
+        RootGroupSlot,
+        SlotCount
+    };
+
+    // JavaScript structure.
+    static const ClassOps classOps;
+    static void finalize(FreeOp* fop, JSObject* obj);
+};
+
+// The class of MeasureGroup instances and MeasureGroup.prototype.
+class MeasureGroupObject : public NativeObject {
+  public:
+    static const Class class_;
+
+    // Add the MeasureGroup constructor to global, using the given measure tree root.
+    static bool DeclareConstructor(JSContext* cx,
+                                   Handle<GlobalObject*> global,
+                                   Handle<MeasurePrototypeObject*> measureProto,
+                                   mozilla::perfstream::GroupPtr&& root);
+
+    // Return the Measure.prototype object.
+    MeasurePrototypeObject& measureProto() {
+        return getReservedSlot(MeasureProtoSlot).toObject().as<MeasurePrototypeObject>();
+    }
+
+    // It would be natural to call this 'group', but JSObject has a method by
+    // that name already.
+    mozilla::perfstream::Group& measureGroup() const {
+        void* group = getReservedSlot(GroupSlot).toPrivate();
+        MOZ_ASSERT(group);
+        return *static_cast<mozilla::perfstream::Group*>(group);
+    }
+
+    // The prototype of a MeasureGroupObject is itself a MeasureGroupObject.
+    using Prototype = MeasureGroupObject;
+
+  private:
+    // Reserved JSObject slot indices.
+    enum {
+        // Measure.prototype, for constructing child Measure instances.
+        MeasureProtoSlot,
+
+        // A Private value pointing to the perfstream::Group this object refers
+        // to. On MeasureGroup.prototype, this refers to the root measure, which
+        // the MeasureGroup and Measure constructors consult for path lookups.
+        GroupSlot,
+
+        SlotCount
+    };
+
+    // C++ utilities.
+    static MeasureGroupObject* create(JSContext*,
+                                      Handle<MeasureGroupObject*> groupProto,
+                                      Handle<MeasurePrototypeObject*> measureProto,
+                                      mozilla::perfstream::GroupPtr&& group);
+    class Args;
+
+    // JavaScript structure.
+    static const ClassSpec classSpec;
+    static const ClassOps classOps;
+    static const JSPropertySpec properties[];
+    static const JSFunctionSpec methods[];
+
+    static MOZ_MUST_USE bool construct(JSContext* cx, unsigned argc, Value* vp);
+    static void finalize(FreeOp* fop, JSObject* obj);
+
+    // Accessors.
+    static MOZ_MUST_USE bool identifierGetter(JSContext* cx, unsigned argc, Value* vp);
+
+    // Methods.
+    static MOZ_MUST_USE bool metadataMethod(JSContext* cx, unsigned argc, Value* vp);
+    static MOZ_MUST_USE bool measuresMethod(JSContext* cx, unsigned argc, Value* vp);
+    static MOZ_MUST_USE bool subgroupsMethod(JSContext* cx, unsigned argc, Value* vp);
+    static MOZ_MUST_USE bool toStringMethod(JSContext* cx, unsigned argc, Value* vp);
+};
+
+// The class of Measure instances.
+class MeasureObject : public NativeObject {
+  public:
+    static const Class class_;
+
+    // Declare the Measure constructor on the given global.
+    static bool DeclareConstructor(JSContext* cx,
+                                   Handle<GlobalObject*> global,
+                                   Handle<MeasurePrototypeObject*> proto);
+
+    static MeasureObject* create(JSContext*,
+                                 HandleObject proto,
+                                 mozilla::perfstream::MeasurePtr&& measure);
+
+    // The prototype of a MeasureObject is itself a MeasurePrototypeObject
+    using Prototype = MeasurePrototypeObject;
+
+  private:
+    // Reserved JSObject slot indices.
+    enum {
+        // A Private value pointing to the perfstream::Measure the instance refers to.
+        MeasureSlot,
+        SlotCount
+    };
+
+    // C++ utilities.
+
+    // Return the measure this MeasureObject refers to.
+    mozilla::perfstream::Measure& measure() const {
+        void* measure = getReservedSlot(MeasureSlot).toPrivate();
+        MOZ_ASSERT(measure);
+        return *static_cast<mozilla::perfstream::Measure*>(measure);
+    }
+
+    class Args;
+
+    // JavaScript structure.
+    static const ClassOps classOps;
+    static const JSPropertySpec properties[];
+    static const JSFunctionSpec methods[];
+
+    static MOZ_MUST_USE bool construct(JSContext* cx, unsigned argc, Value* vp);
+    static void finalize(FreeOp* fop, JSObject* obj);
+
+    // Accessors.
+    static MOZ_MUST_USE bool identifierGetter(JSContext* cx, unsigned argc, Value* vp);
+    static MOZ_MUST_USE bool enabledSetter(JSContext* cx, unsigned argc, Value* vp);
+    static MOZ_MUST_USE bool enabledGetter(JSContext* cx, unsigned argc, Value* vp);
+
+    // Methods.
+    static MOZ_MUST_USE bool metadataMethod(JSContext* cx, unsigned argc, Value* vp);
+    static MOZ_MUST_USE bool toStringMethod(JSContext* cx, unsigned argc, Value* vp);
+};
+
+} // namespace js
+
+#endif // builtin_MeasureObject_h
new file mode 100644
--- /dev/null
+++ b/js/src/jit-test/tests/measures/Measure.js
@@ -0,0 +1,54 @@
+// Measure constructor, methods and properties.
+
+function checkMeasure(measure, identifier) {
+  print('checkMeasure(' + measure + ')');
+
+  // This should be a measure with the given identifier.
+  assertEq(measure instanceof Measure, true);
+  assertEq(measure.identifier, identifier);
+
+  // Looking up the measure from the root should find it.
+  assertEq(Measure(['MeasureTests', identifier]).toString(), measure.toString());
+
+  assertEq(measure.metadata('tooltip-fluent'), 'SpiderMonkey.MeasureTests.toy');
+  assertEq(measure.metadata('tooltip'),
+           'A toy measure for testing the measure machinery. Can be enabled; does nothing.');
+  assertEq(measure.metadata('help-url'),
+           'https://developer.mozilla.org/en-US/docs/Mozilla/Projects/SpiderMonkey');
+}
+
+var flopsy = new Measure(['MeasureTests', 'Flopsy']);
+var mopsy = new Measure(['MeasureTests', 'Mopsy']);
+var cottontail = new Measure(['MeasureTests', 'Cottontail']);
+
+checkMeasure(flopsy, 'Flopsy');
+checkMeasure(mopsy, 'Mopsy');
+checkMeasure(cottontail, 'Cottontail');
+
+// All MeasureTest measures of a given path refer to the same bit.
+var flopsy2 = new Measure(['MeasureTests', 'Flopsy']);
+var mopsy2 = new Measure(['MeasureTests', 'Mopsy']);
+
+flopsy.enabled = true;
+assertEq(flopsy2.enabled, true);
+assertEq(mopsy.enabled, false);
+assertEq(mopsy2.enabled, false);
+assertEq(flopsy.enabled, true);
+
+flopsy2.enabled = false;
+assertEq(flopsy.enabled, false);
+assertEq(mopsy.enabled, false);
+assertEq(mopsy2.enabled, false);
+assertEq(flopsy2.enabled, false);
+
+mopsy.enabled = true;
+assertEq(flopsy.enabled, false);
+assertEq(flopsy2.enabled, false);
+assertEq(mopsy2.enabled, true);
+assertEq(mopsy.enabled, true);
+
+mopsy.enabled = false;
+assertEq(flopsy.enabled, false);
+assertEq(flopsy2.enabled, false);
+assertEq(mopsy2.enabled, false);
+assertEq(mopsy.enabled, false);
new file mode 100644
--- /dev/null
+++ b/js/src/jit-test/tests/measures/MeasureGroup.js
@@ -0,0 +1,62 @@
+// MeasureGroup constructor, methods and properties.
+
+const root = new MeasureGroup();
+assertEq(root instanceof MeasureGroup, true);
+assertEq(root.identifier, 'SpiderMonkey');
+assertEq(root.metadata('tooltip'), 'the SpiderMonkey JavaScript engine');
+assertEq(root.metadata('tooltip-fluent'), 'SpiderMonkey.measureGroup.description');
+assertEq(root.metadata('help-url'),
+         'https://developer.mozilla.org/en-US/docs/Mozilla/Projects/SpiderMonkey');
+assertEq(root.metadata('santa'), undefined);
+
+function checkGroup(group, path) {
+  print('checkGroup(' + group, ', ' + uneval(path) + ')');
+
+  const measures = group.measures();
+
+  // The 'measures' method should return an array of Measures.
+  assertEq(Array.isArray(measures), true);
+  assertEq(measures.every(elt => elt instanceof Measure), true);
+
+  // Every measure mentioned should also be findable by its path from the root.
+  for (const measure of measures) {
+    const byPath = Measure(path.concat([measure.identifier]));
+    assertEq(measure.toString(), byPath.toString());
+  }
+
+  const subgroups = group.subgroups();
+
+  // The 'subgroups' method should return an array of MeasureGroups.
+  assertEq(Array.isArray(subgroups), true);
+  assertEq(subgroups.every(elt => elt instanceof MeasureGroup), true);
+
+  // Every group mentioned should also be findable by its path from the root.
+  for (const subgroup of subgroups) {
+    const byPath = MeasureGroup(path.concat([subgroup.identifier]));
+    assertEq(subgroup.toString(), byPath.toString());
+  }
+}
+
+checkGroup(root, []);
+
+// The root group should have a child named 'MeasureTests'.
+const rootSubgroups = root.subgroups();
+assertEq(rootSubgroups.filter(elt => elt.identifier == 'MeasureTests').length, 1);
+
+const tests = new MeasureGroup(['MeasureTests']);
+checkGroup(tests, ['MeasureTests']);
+
+assertEq(tests instanceof MeasureGroup, true);
+assertEq(tests.identifier, 'MeasureTests')
+assertEq(tests.metadata('tooltip'), 'Test measure group for SpiderMonkey.');
+assertEq(tests.metadata('prams'), 'definitely');
+assertEq(tests.metadata('prawns'), 'probably');
+assertEq(tests.metadata('tooltip-fluent'), undefined);
+
+assertEq(tests.subgroups().length, 0);
+
+const testMeasures = tests.measures();
+assertEq(testMeasures.length, 3);
+assertEq(testMeasures.every(elt => {
+  return ['Flopsy', 'Mopsy', 'Cottontail'].includes(elt.identifier);
+}), true);
new file mode 100644
--- /dev/null
+++ b/js/src/jit-test/tests/measures/attributes.js
@@ -0,0 +1,49 @@
+// Check basic attributes on Measure, MeasureGroup, and their prototypes.
+
+// Prototypes should be unchangeable. The C++ asserts that, given that the
+// instance's class is X, the prototype's class must be X as well.
+function checkPrototype(constructor) {
+  print('checkPrototype(' + constructor.name + ')');
+  const desc = Object.getOwnPropertyDescriptor(constructor, 'prototype');
+  assertEq(typeof desc.value, 'object');
+  assertEq(desc.writable, false);
+  assertEq(desc.configurable, false);
+  assertEq(desc.enumerable, false);
+}
+
+checkPrototype(Measure);
+checkPrototype(MeasureGroup);
+
+// Accessor properties should indeed be accessors, and configurable.
+function checkAccessor(constructor, name, hasSetter) {
+  print('checkAccessor(' + constructor.name + ', '+ uneval(name) + ')');
+  const desc = Object.getOwnPropertyDescriptor(constructor.prototype, name);
+  assertEq(typeof desc.get, 'function');
+  assertEq(typeof desc.set, hasSetter ? 'function' : 'undefined');
+  assertEq(desc.configurable, true);
+  assertEq(desc.enumerable, false);
+}
+
+checkAccessor(Measure, 'enabled', true);
+checkAccessor(Measure, 'identifier');
+
+checkAccessor(MeasureGroup, 'identifier');
+
+// Methods should be value properties referring to functions, writable and
+// configurable.
+function checkMethod(constructor, name) {
+  print('checkMethod(' + constructor.name + ', ' + uneval(name) + ')');
+  const desc = Object.getOwnPropertyDescriptor(constructor.prototype, name);
+  assertEq(typeof desc.value, 'function');
+  assertEq(desc.writable, true);
+  assertEq(desc.configurable, true);
+  assertEq(desc.enumerable, false);
+}
+
+checkMethod(Measure, 'metadata');
+checkMethod(Measure, 'toString');
+
+checkMethod(MeasureGroup, 'metadata');
+checkMethod(MeasureGroup, 'measures');
+checkMethod(MeasureGroup, 'subgroups');
+checkMethod(MeasureGroup, 'toString');
new file mode 100644
--- /dev/null
+++ b/js/src/jit-test/tests/measures/switch-proto.js
@@ -0,0 +1,10 @@
+// Make sure we don't crash if someone changes our prototype.
+
+load(libdir + 'asserts.js');
+
+// Interpose an ordinary object on mt's prototype chain, and then try to look up
+// subgroups.
+const mt = MeasureGroup(['MeasureTests']);
+mt.__proto__ = Object.create(MeasureGroup.prototype);
+assertThrowsInstanceOf(() => mt.subgroups().map(x => x.identifier), TypeError);
+
--- a/js/src/js.msg
+++ b/js/src/js.msg
@@ -111,16 +111,17 @@ MSG_DEF(JSMSG_NOT_OBJORNULL,           1
 // JSON
 MSG_DEF(JSMSG_JSON_BAD_PARSE,          3, JSEXN_SYNTAXERR, "JSON.parse: {0} at line {1} column {2} of the JSON data")
 MSG_DEF(JSMSG_JSON_CYCLIC_VALUE,       0, JSEXN_TYPEERR, "cyclic object value")
 
 // Runtime errors
 MSG_DEF(JSMSG_BAD_INSTANCEOF_RHS,      1, JSEXN_TYPEERR, "invalid 'instanceof' operand {0}")
 MSG_DEF(JSMSG_BAD_LEFTSIDE_OF_ASS,     0, JSEXN_REFERENCEERR, "invalid assignment left-hand side")
 MSG_DEF(JSMSG_BAD_PROTOTYPE,           1, JSEXN_TYPEERR, "'prototype' property of {0} is not an object")
+MSG_DEF(JSMSG_BAD_PROTOTYPE_TYPE,      1, JSEXN_TYPEERR, "'prototype' property of {0} is not an object of the required kind")
 MSG_DEF(JSMSG_IN_NOT_OBJECT,           1, JSEXN_TYPEERR, "invalid 'in' operand {0}")
 MSG_DEF(JSMSG_TOO_MANY_CON_SPREADARGS, 0, JSEXN_RANGEERR, "too many constructor arguments")
 MSG_DEF(JSMSG_TOO_MANY_FUN_SPREADARGS, 0, JSEXN_RANGEERR, "too many function arguments")
 MSG_DEF(JSMSG_UNINITIALIZED_LEXICAL,   1, JSEXN_REFERENCEERR, "can't access lexical declaration `{0}' before initialization")
 MSG_DEF(JSMSG_BAD_CONST_ASSIGN,        1, JSEXN_TYPEERR, "invalid assignment to const `{0}'")
 MSG_DEF(JSMSG_CANT_DECLARE_GLOBAL_BINDING, 2, JSEXN_TYPEERR, "cannot declare global binding `{0}': {1}")
 
 // Date
@@ -636,8 +637,11 @@ MSG_DEF(JSMSG_STREAM_INVALID_HIGHWATERMA
 
 // Response-related
 MSG_DEF(JSMSG_ERROR_CONSUMING_RESPONSE,                  0, JSEXN_TYPEERR,  "there was an error consuming the Response")
 MSG_DEF(JSMSG_BAD_RESPONSE_VALUE,                        0, JSEXN_TYPEERR,  "expected Response or Promise resolving to Response")
 MSG_DEF(JSMSG_BAD_RESPONSE_MIME_TYPE,                    0, JSEXN_TYPEERR,  "Response has unsupported MIME type")
 MSG_DEF(JSMSG_BAD_RESPONSE_CORS_SAME_ORIGIN,             0, JSEXN_TYPEERR,  "Response.type must be 'basic', 'cors' or 'default'")
 MSG_DEF(JSMSG_BAD_RESPONSE_STATUS,                       0, JSEXN_TYPEERR,  "Response does not have ok status")
 MSG_DEF(JSMSG_RESPONSE_ALREADY_CONSUMED,                 0, JSEXN_TYPEERR,  "Response already consumed")
+
+// Measure
+MSG_DEF(JSMSG_MEASURE_NO_PATH,         1, JSEXN_TYPEERR, "No Measure in tree named {0}")
--- a/js/src/moz.build
+++ b/js/src/moz.build
@@ -147,16 +147,17 @@ EXPORTS.js += [
 ]
 
 UNIFIED_SOURCES += [
     'builtin/AtomicsObject.cpp',
     'builtin/DataViewObject.cpp',
     'builtin/Eval.cpp',
     'builtin/Intl.cpp',
     'builtin/MapObject.cpp',
+    'builtin/MeasureObject.cpp',
     'builtin/ModuleObject.cpp',
     'builtin/Object.cpp',
     'builtin/Profilers.cpp',
     'builtin/Promise.cpp',
     'builtin/Reflect.cpp',
     'builtin/ReflectParse.cpp',
     'builtin/SIMD.cpp',
     'builtin/Stream.cpp',
--- a/js/src/shell/js.cpp
+++ b/js/src/shell/js.cpp
@@ -73,16 +73,17 @@
 #include "jit/InlinableNatives.h"
 #include "jit/Ion.h"
 #include "jit/JitcodeMap.h"
 #include "jit/OptimizationTracking.h"
 #include "js/Debug.h"
 #include "js/GCAPI.h"
 #include "js/GCVector.h"
 #include "js/Initialization.h"
+#include "js/Measure.h"
 #include "js/StructuredClone.h"
 #include "js/SweepingAPI.h"
 #include "js/TrackedOptimizationInfo.h"
 #include "perf/jsperf.h"
 #include "shell/jsoptparse.h"
 #include "shell/jsshell.h"
 #include "shell/OSObject.h"
 #include "threading/ConditionVariable.h"
@@ -7940,16 +7941,22 @@ NewGlobalObject(JSContext* cx, JS::Compa
 
         if (!fuzzingSafe) {
             if (!JS_DefineFunctionsWithHelp(cx, glob, fuzzing_unsafe_functions))
                 return nullptr;
             if (!DefineConsole(cx, glob))
                 return nullptr;
         }
 
+        mozilla::perfstream::GroupPtr measureRoot = JS::GetMeasuresWithTests(cx);
+        if (!measureRoot)
+            return nullptr;
+        if (!JS::DefineMeasureConstructors(cx, glob, Move(measureRoot)))
+            return nullptr;
+
         if (!DefineOS(cx, glob, fuzzingSafe, &gOutFile, &gErrFile))
             return nullptr;
 
         RootedObject performanceObj(cx, JS_NewObject(cx, nullptr));
         if (!performanceObj)
             return nullptr;
         RootedObject mozMemoryObj(cx, JS_NewObject(cx, nullptr));
         if (!mozMemoryObj)
--- a/js/src/shell/moz.build
+++ b/js/src/shell/moz.build
@@ -36,16 +36,18 @@ LOCAL_INCLUDES += [
 OS_LIBS += CONFIG['EDITLINE_LIBS']
 OS_LIBS += CONFIG['MOZ_ZLIB_LIBS']
 
 if CONFIG['ENABLE_INTL_API'] and CONFIG['MOZ_ICU_DATA_ARCHIVE']:
     # The ICU libraries linked into libmozjs will not include the ICU data,
     # so link it directly.
     USE_LIBS += ['icudata']
 
+USE_LIBS += ['perfstream']
+
 # Prepare module loader JS code for embedding
 GENERATED_FILES += ['shellmoduleloader.out.h']
 shellmoduleloader = GENERATED_FILES['shellmoduleloader.out.h']
 shellmoduleloader.script = '../builtin/embedjs.py:generate_shellmoduleloader'
 shellmoduleloader.inputs = [
     '../js.msg',
     'ModuleLoader.js',
 ]