Bug 1545582 - Add a JS runtime level of allocation logging; r=jimb
☠☠ backed out by c945fb0af695 ☠ ☠
authorGreg Tatum <gtatum@mozilla.com>
Fri, 14 Jun 2019 20:18:22 +0000
changeset 478978 285673afaa99a6097e244fb0c1ba0b2b6945893d
parent 478977 311baee18a93f0564bf0928f913a9ce6faef0332
child 478979 699de45940b18e0bc27ccc26228546b7eaec785d
push id88006
push usergtatum@mozilla.com
push dateFri, 14 Jun 2019 22:36:16 +0000
treeherderautoland@a47c4a44bae0 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersjimb
bugs1545582
milestone69.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 1545582 - Add a JS runtime level of allocation logging; r=jimb Differential Revision: https://phabricator.services.mozilla.com/D28142
js/public/AllocationRecording.h
js/public/UbiNode.h
js/src/jsapi.cpp
js/src/moz.build
js/src/vm/Debugger.cpp
js/src/vm/Debugger.h
js/src/vm/Realm.cpp
js/src/vm/Realm.h
js/src/vm/Runtime.cpp
js/src/vm/Runtime.h
js/src/vm/SavedStacks.cpp
js/src/vm/SavedStacks.h
js/src/vm/UbiNode.cpp
new file mode 100644
--- /dev/null
+++ b/js/public/AllocationRecording.h
@@ -0,0 +1,75 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*-
+ * vim: set ts=8 sts=2 et sw=2 tw=80:
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef js_AllocationRecording_h
+#define js_AllocationRecording_h
+
+#include "js/TypeDecls.h"
+#include "js/Utility.h"
+
+namespace JS {
+
+/**
+ * This struct holds the information needed to create a profiler marker payload
+ * that can represent a JS allocation. It translates JS engine specific classes,
+ * into something that can be used in the profiler.
+ */
+struct RecordAllocationInfo {
+  RecordAllocationInfo(const char16_t* typeName, const char* className,
+                       const char16_t* descriptiveTypeName,
+                       const char* scriptFilename, const char* coarseType,
+                       size_t size, bool inNursery)
+      : typeName(typeName),
+        className(className),
+        descriptiveTypeName(descriptiveTypeName),
+        scriptFilename(scriptFilename),
+        coarseType(coarseType),
+        size(size),
+        inNursery(inNursery) {}
+
+  // These pointers are borrowed from the UbiNode, and can point to live data.
+  // It is important for the consumers of this struct to correctly
+  // duplicate the strings to take ownership of them.
+  const char16_t* typeName;
+  const char* className;
+  const char16_t* descriptiveTypeName;
+  const char* scriptFilename;
+
+  // The coarseType points to a string literal, so does not need to be
+  // duplicated.
+  const char* coarseType;
+
+  // The size in bytes of the allocation.
+  size_t size;
+
+  // Whether or not the allocation is in the nursery or not.
+  bool inNursery;
+};
+
+typedef void (*RecordAllocationsCallback)(RecordAllocationInfo&& info);
+
+/**
+ * Enable recording JS allocations. This feature hooks into the object creation
+ * in the JavaScript engine, and reports back the allocation info through the
+ * callback. This allocation tracking is turned on for all encountered realms.
+ * The JS Debugger API can also turn on allocation tracking with its own
+ * probability. If both allocation tracking mechanisms are turned on at the same
+ * time, the Debugger's probability defers to the EnableRecordingAllocations's
+ * probability setting.
+ */
+JS_FRIEND_API void EnableRecordingAllocations(
+    JSContext* cx, RecordAllocationsCallback callback, double probability);
+
+/**
+ * Turn off JS allocation recording. If any JS Debuggers are also recording
+ * allocations, then the probability will be reset to the Debugger's desired
+ * setting.
+ */
+JS_FRIEND_API void DisableRecordingAllocations(JSContext* cx);
+
+}  // namespace JS
+
+#endif /* js_AllocationRecording_h */
--- a/js/public/UbiNode.h
+++ b/js/public/UbiNode.h
@@ -504,16 +504,21 @@ enum class CoarseType : uint32_t {
   Script = 2,
   String = 3,
   DOMNode = 4,
 
   FIRST = Other,
   LAST = DOMNode
 };
 
+/**
+ * Convert a CoarseType enum into a string. The string is statically allocated.
+ */
+JS_PUBLIC_API const char* CoarseTypeToString(CoarseType type);
+
 inline uint32_t CoarseTypeToUint32(CoarseType type) {
   return static_cast<uint32_t>(type);
 }
 
 inline bool Uint32IsValidCoarseType(uint32_t n) {
   auto first = static_cast<uint32_t>(CoarseType::FIRST);
   auto last = static_cast<uint32_t>(CoarseType::LAST);
   MOZ_ASSERT(first < last);
--- a/js/src/jsapi.cpp
+++ b/js/src/jsapi.cpp
@@ -1702,16 +1702,17 @@ JS_PUBLIC_API void JS_FireOnNewGlobalObj
                                             JS::HandleObject global) {
   // This hook is infallible, because we don't really want arbitrary script
   // to be able to throw errors during delicate global creation routines.
   // This infallibility will eat OOM and slow script, but if that happens
   // we'll likely run up into them again soon in a fallible context.
   cx->check(global);
   Rooted<js::GlobalObject*> globalObject(cx, &global->as<GlobalObject>());
   Debugger::onNewGlobalObject(cx, globalObject);
+  cx->runtime()->ensureRealmIsRecordingAllocations(globalObject);
 }
 
 JS_PUBLIC_API JSObject* JS_NewObject(JSContext* cx, const JSClass* jsclasp) {
   MOZ_ASSERT(!cx->zone()->isAtomsZone());
   AssertHeapIsIdle();
   CHECK_THREAD(cx);
 
   const Class* clasp = Valueify(jsclasp);
--- a/js/src/moz.build
+++ b/js/src/moz.build
@@ -111,16 +111,17 @@ EXPORTS += [
     'jsapi.h',
     'jsfriendapi.h',
     'jspubtd.h',
     'jstypes.h',
     'perf/jsperf.h',
 ]
 
 EXPORTS.js += [
+    '../public/AllocationRecording.h',
     '../public/AllocPolicy.h',
     '../public/ArrayBuffer.h',
     '../public/BuildId.h',
     '../public/CallArgs.h',
     '../public/CallNonGenericMethod.h',
     '../public/CharacterEncoding.h',
     '../public/Class.h',
     '../public/CompilationAndEvaluation.h',
--- a/js/src/vm/Debugger.cpp
+++ b/js/src/vm/Debugger.cpp
@@ -3322,17 +3322,22 @@ void Debugger::removeAllocationsTracking
   // If there are still Debuggers that are observing allocations, we cannot
   // remove the metadata callback yet. Recompute the sampling probability
   // based on the remaining debuggers' needs.
   if (isObservedByDebuggerTrackingAllocations(global)) {
     global.realm()->chooseAllocationSamplingProbability();
     return;
   }
 
-  global.realm()->forgetAllocationMetadataBuilder();
+  if (!global.realm()->runtimeFromMainThread()->recordAllocationCallback) {
+    // Something like the Gecko Profiler could request from the the JS runtime
+    // to record allocations. If it is recording allocations, then do not
+    // destroy the allocation metadata builder at this time.
+    global.realm()->forgetAllocationMetadataBuilder();
+  }
 }
 
 bool Debugger::addAllocationsTrackingForAllDebuggees(JSContext* cx) {
   MOZ_ASSERT(trackingAllocationSites);
 
   // We don't want to end up in a state where we added allocations
   // tracking to some of our debuggees, but failed to do so for
   // others. Before attempting to start tracking allocations in *any* of
--- a/js/src/vm/Debugger.h
+++ b/js/src/vm/Debugger.h
@@ -386,16 +386,22 @@ class Debugger : private mozilla::Linked
   // Barrier methods so we can have WeakHeapPtr<Debugger*>.
   static void readBarrier(Debugger* dbg) {
     InternalBarrierMethods<JSObject*>::readBarrier(dbg->object);
   }
   static void writeBarrierPost(Debugger** vp, Debugger* prev, Debugger* next) {}
 #ifdef DEBUG
   static void assertThingIsNotGray(Debugger* dbg) { return; }
 #endif
+  /*
+   * Return true if the given global is being observed by at least one
+   * Debugger that is tracking allocations.
+   */
+  static bool isObservedByDebuggerTrackingAllocations(
+      const GlobalObject& debuggee);
 
  private:
   GCPtrNativeObject object; /* The Debugger object. Strong reference. */
   WeakGlobalObjectSet
       debuggees; /* Debuggee globals. Cross-compartment weak references. */
   JS::ZoneSet debuggeeZones; /* Set of zones that we have debuggees in. */
   js::GCPtrObject uncaughtExceptionHook; /* Strong reference. */
   bool enabled;
@@ -445,23 +451,16 @@ class Debugger : private mozilla::Linked
   /*
    * Return true if there is an existing object metadata callback for the
    * given global's compartment that will prevent our instrumentation of
    * allocations.
    */
   static bool cannotTrackAllocations(const GlobalObject& global);
 
   /*
-   * Return true if the given global is being observed by at least one
-   * Debugger that is tracking allocations.
-   */
-  static bool isObservedByDebuggerTrackingAllocations(
-      const GlobalObject& global);
-
-  /*
    * Add allocations tracking for objects allocated within the given
    * debuggee's compartment. The given debuggee global must be observed by at
    * least one Debugger that is enabled and tracking allocations.
    */
   static MOZ_MUST_USE bool addAllocationsTracking(
       JSContext* cx, Handle<GlobalObject*> debuggee);
 
   /*
--- a/js/src/vm/Realm.cpp
+++ b/js/src/vm/Realm.cpp
@@ -608,26 +608,33 @@ void Realm::clearTables() {
   MOZ_ASSERT(!debugEnvs_);
   MOZ_ASSERT(objects_.enumerators->next() == objects_.enumerators);
 
   objectGroups_.clearTables();
   savedStacks_.clear();
   varNames_.clear();
 }
 
+// Check to see if this individual realm is recording allocations. Debuggers or
+// runtimes can try and record allocations, so this method can check to see if
+// any initialization is needed.
+bool Realm::isRecordingAllocations() { return !!allocationMetadataBuilder_; }
+
 void Realm::setAllocationMetadataBuilder(
     const js::AllocationMetadataBuilder* builder) {
   // Clear any jitcode in the runtime, which behaves differently depending on
   // whether there is a creation callback.
   ReleaseAllJITCode(runtime_->defaultFreeOp());
 
   allocationMetadataBuilder_ = builder;
 }
 
 void Realm::forgetAllocationMetadataBuilder() {
+  MOZ_ASSERT(allocationMetadataBuilder_);
+
   // Unlike setAllocationMetadataBuilder, we don't have to discard all JIT
   // code here (code is still valid, just a bit slower because it doesn't do
   // inline GC allocations when a metadata builder is present), but we do want
   // to cancel off-thread Ion compilations to avoid races when Ion calls
   // hasAllocationMetadataBuilder off-thread.
   CancelOffThreadIonCompile(this);
 
   allocationMetadataBuilder_ = nullptr;
--- a/js/src/vm/Realm.h
+++ b/js/src/vm/Realm.h
@@ -600,16 +600,17 @@ class JS::Realm : public JS::shadow::Rea
     return allocationMetadataBuilder_;
   }
   const js::AllocationMetadataBuilder* getAllocationMetadataBuilder() const {
     return allocationMetadataBuilder_;
   }
   const void* addressOfMetadataBuilder() const {
     return &allocationMetadataBuilder_;
   }
+  bool isRecordingAllocations();
   void setAllocationMetadataBuilder(
       const js::AllocationMetadataBuilder* builder);
   void forgetAllocationMetadataBuilder();
   void setNewObjectMetadata(JSContext* cx, JS::HandleObject obj);
 
   bool hasObjectPendingMetadata() const {
     return objectMetadataState_.is<js::PendingMetadata>();
   }
@@ -792,18 +793,19 @@ class JS::Realm : public JS::shadow::Rea
 
   js::DebugEnvironments* debugEnvs() { return debugEnvs_.get(); }
   js::UniquePtr<js::DebugEnvironments>& debugEnvsRef() { return debugEnvs_; }
 
   js::SavedStacks& savedStacks() { return savedStacks_; }
 
   // Recompute the probability with which this realm should record
   // profiling data (stack traces, allocations log, etc.) about each
-  // allocation. We consult the probabilities requested by the Debugger
-  // instances observing us, if any.
+  // allocation. We first consult the JS runtime to see if it is recording
+  // allocations, and if not then check the probabilities requested by the
+  // Debugger instances observing us, if any.
   void chooseAllocationSamplingProbability() {
     savedStacks_.chooseSamplingProbability(this);
   }
 
   void sweepSavedStacks();
 
   static constexpr size_t offsetOfCompartment() {
     return offsetof(JS::Realm, compartment_);
--- a/js/src/vm/Runtime.cpp
+++ b/js/src/vm/Runtime.cpp
@@ -842,12 +842,67 @@ JS_FRIEND_API void JS::SetJSContextProfi
   cx->runtime()->setProfilerSampleBufferRangeStart(rangeStart);
 }
 
 JS_FRIEND_API bool JS::IsProfilingEnabledForContext(JSContext* cx) {
   MOZ_ASSERT(cx);
   return cx->runtime()->geckoProfiler().enabled();
 }
 
+JS_FRIEND_API void JS::EnableRecordingAllocations(
+    JSContext* cx, JS::RecordAllocationsCallback callback, double probability) {
+  MOZ_ASSERT(cx);
+  MOZ_ASSERT(cx->isMainThreadContext());
+  cx->runtime()->startRecordingAllocations(probability, callback);
+}
+
+JS_FRIEND_API void JS::DisableRecordingAllocations(JSContext* cx) {
+  MOZ_ASSERT(cx);
+  MOZ_ASSERT(cx->isMainThreadContext());
+  cx->runtime()->stopRecordingAllocations();
+}
+
 JS_PUBLIC_API void JS::shadow::RegisterWeakCache(
     JSRuntime* rt, detail::WeakCacheBase* cachep) {
   rt->registerWeakCache(cachep);
 }
+
+void JSRuntime::startRecordingAllocations(
+    double probability, JS::RecordAllocationsCallback callback) {
+  allocationSamplingProbability = probability;
+  recordAllocationCallback = callback;
+
+  // Go through all of the existing realms, and turn on allocation tracking.
+  for (RealmsIter realm(this); !realm.done(); realm.next()) {
+    realm->setAllocationMetadataBuilder(&SavedStacks::metadataBuilder);
+    realm->chooseAllocationSamplingProbability();
+  }
+}
+
+void JSRuntime::stopRecordingAllocations() {
+  recordAllocationCallback = nullptr;
+  // Go through all of the existing realms, and turn on allocation tracking.
+  for (RealmsIter realm(this); !realm.done(); realm.next()) {
+    js::GlobalObject* global = realm->maybeGlobal();
+    if (!realm->isDebuggee() || !global ||
+        !Debugger::isObservedByDebuggerTrackingAllocations(*global)) {
+      // Only remove the allocation metadata builder if no Debuggers are
+      // tracking allocations.
+      realm->forgetAllocationMetadataBuilder();
+    }
+  }
+}
+
+// This function can run to ensure that when new realms are created
+// they have allocation logging turned on.
+void JSRuntime::ensureRealmIsRecordingAllocations(
+    Handle<GlobalObject*> global) {
+  if (recordAllocationCallback) {
+    if (!global->realm()->isRecordingAllocations()) {
+      // This is a new realm, turn on allocations for it.
+      global->realm()->setAllocationMetadataBuilder(
+          &SavedStacks::metadataBuilder);
+    }
+    // Ensure the probability is up to date with the current combination of
+    // debuggers and runtime profiling.
+    global->realm()->chooseAllocationSamplingProbability();
+  }
+}
--- a/js/src/vm/Runtime.h
+++ b/js/src/vm/Runtime.h
@@ -24,16 +24,17 @@
 #include "builtin/AtomicsObject.h"
 #include "builtin/intl/SharedIntlData.h"
 #include "builtin/Promise.h"
 #include "frontend/BinASTRuntimeSupport.h"
 #include "frontend/NameCollections.h"
 #include "gc/GCRuntime.h"
 #include "gc/Tracer.h"
 #include "irregexp/RegExpStack.h"
+#include "js/AllocationRecording.h"
 #include "js/BuildId.h"  // JS::BuildIdOp
 #include "js/Debug.h"
 #include "js/experimental/SourceHook.h"  // js::SourceHook
 #include "js/GCVector.h"
 #include "js/HashTable.h"
 #include "js/Modules.h"  // JS::Module{DynamicImport,Metadata,Resolve}Hook
 #ifdef DEBUG
 #  include "js/Proxy.h"  // For AutoEnterPolicy
@@ -519,32 +520,42 @@ struct JSRuntime : public js::MallocProv
 
   JS::HeapState heapState() const { return heapState_; }
 
   // How many realms there are across all zones. This number includes
   // off-thread context realms, so it isn't necessarily equal to the
   // number of realms visited by RealmsIter.
   js::MainThreadData<size_t> numRealms;
 
+  // The Gecko Profiler may want to sample the allocations happening across the
+  // browser. This callback can be registered to record the allocation.
+  js::MainThreadData<JS::RecordAllocationsCallback> recordAllocationCallback;
+  js::MainThreadData<double> allocationSamplingProbability;
+
  private:
   // Number of debuggee realms in the runtime.
   js::MainThreadData<size_t> numDebuggeeRealms_;
 
   // Number of debuggee realms in the runtime observing code coverage.
   js::MainThreadData<size_t> numDebuggeeRealmsObservingCoverage_;
 
  public:
   void incrementNumDebuggeeRealms();
   void decrementNumDebuggeeRealms();
 
   size_t numDebuggeeRealms() const { return numDebuggeeRealms_; }
 
   void incrementNumDebuggeeRealmsObservingCoverage();
   void decrementNumDebuggeeRealmsObservingCoverage();
 
+  void startRecordingAllocations(double probability,
+                                 JS::RecordAllocationsCallback callback);
+  void stopRecordingAllocations();
+  void ensureRealmIsRecordingAllocations(JS::Handle<js::GlobalObject*> global);
+
   /* Locale-specific callbacks for string conversion. */
   js::MainThreadData<const JSLocaleCallbacks*> localeCallbacks;
 
   /* Default locale for Internationalization API */
   js::MainThreadData<js::UniqueChars> defaultLocale;
 
   /* If true, new scripts must be created with PC counter information. */
   js::MainThreadOrIonCompileData<bool> profilingScripts;
--- a/js/src/vm/SavedStacks.cpp
+++ b/js/src/vm/SavedStacks.cpp
@@ -1793,16 +1793,26 @@ bool SavedStacks::getLocation(JSContext*
     }
   }
 
   locationp.set(p->value());
   return true;
 }
 
 void SavedStacks::chooseSamplingProbability(Realm* realm) {
+  {
+    JSRuntime* runtime = realm->runtimeFromMainThread();
+    if (runtime->recordAllocationCallback) {
+      // The runtime is tracking allocations across all realms, in this case
+      // ignore all of the debugger values, and use the runtime's probability.
+      this->setSamplingProbability(runtime->allocationSamplingProbability);
+      return;
+    }
+  }
+
   // Use unbarriered version to prevent triggering read barrier while
   // collecting, this is safe as long as global does not escape.
   GlobalObject* global = realm->unsafeUnbarrieredMaybeGlobal();
   if (!global) {
     return;
   }
 
   GlobalObject::DebuggerVector* dbgs = global->getDebuggers();
@@ -1824,16 +1834,20 @@ void SavedStacks::chooseSamplingProbabil
 
     if (dbgp->trackingAllocationSites && dbgp->enabled) {
       foundAnyDebuggers = true;
       probability = std::max(dbgp->allocationSamplingProbability, probability);
     }
   }
   MOZ_ASSERT(foundAnyDebuggers);
 
+  this->setSamplingProbability(probability);
+}
+
+void SavedStacks::setSamplingProbability(double probability) {
   if (!bernoulliSeeded) {
     mozilla::Array<uint64_t, 2> seed;
     GenerateXorShift128PlusSeed(seed);
     bernoulli.setRandomState(seed[0], seed[1]);
     bernoulliSeeded = true;
   }
 
   bernoulli.setProbability(probability);
@@ -1854,16 +1868,39 @@ JSObject* SavedStacks::MetadataBuilder::
     oomUnsafe.crash("SavedStacksMetadataBuilder");
   }
 
   if (!Debugger::onLogAllocationSite(cx, obj, frame,
                                      mozilla::TimeStamp::Now())) {
     oomUnsafe.crash("SavedStacksMetadataBuilder");
   }
 
+  auto recordAllocationCallback =
+      cx->realm()->runtimeFromMainThread()->recordAllocationCallback;
+  if (recordAllocationCallback) {
+    // The following code translates the JS-specific information, into an
+    // RecordAllocationInfo object that can be consumed outside of SpiderMonkey.
+
+    // Do not GC during this operation, strings are being copied out of the JS
+    // engine.
+    AutoCheckCannotGC nogc;
+
+    auto node = JS::ubi::Node(obj.get());
+
+    // Pass the non-SpiderMonkey specific information back to the
+    // callback to get it out of the JS engine. Strings will need to be
+    // copied by the callback. After it is done we release the
+    // AutoCheckCannotGC.
+    recordAllocationCallback(JS::RecordAllocationInfo{
+        node.typeName(), node.jsObjectClassName(), node.descriptiveTypeName(),
+        node.scriptFilename(), JS::ubi::CoarseTypeToString(node.coarseType()),
+        node.size(cx->runtime()->debuggerMallocSizeOf),
+        gc::IsInsideNursery(obj)});
+  }
+
   MOZ_ASSERT_IF(frame, !frame->is<WrapperObject>());
   return frame;
 }
 
 const SavedStacks::MetadataBuilder SavedStacks::metadataBuilder;
 
 /* static */
 ReconstructedSavedFramePrincipals ReconstructedSavedFramePrincipals::IsSystem;
--- a/js/src/vm/SavedStacks.h
+++ b/js/src/vm/SavedStacks.h
@@ -222,16 +222,17 @@ class SavedStacks {
       JSContext* cx, MutableHandleSavedFrame asyncStack, HandleAtom asyncCause,
       const mozilla::Maybe<size_t>& maxFrameCount);
   MOZ_MUST_USE bool checkForEvalInFramePrev(
       JSContext* cx, MutableHandle<SavedFrame::Lookup> lookup);
   SavedFrame* getOrCreateSavedFrame(JSContext* cx,
                                     Handle<SavedFrame::Lookup> lookup);
   SavedFrame* createFrameFromLookup(JSContext* cx,
                                     Handle<SavedFrame::Lookup> lookup);
+  void setSamplingProbability(double probability);
 
   // Cache for memoizing PCToLineNumber lookups.
 
   struct PCKey {
     PCKey(JSScript* script, jsbytecode* pc) : script(script), pc(pc) {}
 
     HeapPtr<JSScript*> script;
     jsbytecode* pc;
--- a/js/src/vm/UbiNode.cpp
+++ b/js/src/vm/UbiNode.cpp
@@ -519,10 +519,27 @@ void Concrete<JSObject>::construct(void*
 }
 
 void SetConstructUbiNodeForDOMObjectCallback(JSContext* cx,
                                              void (*callback)(void*,
                                                               JSObject*)) {
   cx->runtime()->constructUbiNodeForDOMObjectCallback = callback;
 }
 
+JS_PUBLIC_API const char* CoarseTypeToString(CoarseType type) {
+  switch (type) {
+    case CoarseType::Other:
+      return "Other";
+    case CoarseType::Object:
+      return "Object";
+    case CoarseType::Script:
+      return "Script";
+    case CoarseType::String:
+      return "String";
+    case CoarseType::DOMNode:
+      return "DOMNode";
+    default:
+      return "Unknown";
+  }
+};
+
 }  // namespace ubi
 }  // namespace JS