Bug 1545582 - Add a JS runtime level of allocation logging; r=jimb

Differential Revision: https://phabricator.services.mozilla.com/D28142

--HG--
extra : moz-landing-system : lando
This commit is contained in:
Greg Tatum 2019-06-19 21:07:52 +00:00
Родитель 438420c8fc
Коммит c46b8c0571
13 изменённых файлов: 226 добавлений и 11 удалений

Просмотреть файл

@ -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,
uint64_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.
uint64_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 */

Просмотреть файл

@ -509,6 +509,11 @@ enum class CoarseType : uint32_t {
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);
}

Просмотреть файл

@ -1707,6 +1707,7 @@ JS_PUBLIC_API void JS_FireOnNewGlobalObject(JSContext* cx,
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) {

Просмотреть файл

@ -116,6 +116,7 @@ EXPORTS += [
]
EXPORTS.js += [
'../public/AllocationRecording.h',
'../public/AllocPolicy.h',
'../public/ArrayBuffer.h',
'../public/BuildId.h',

Просмотреть файл

@ -3327,7 +3327,12 @@ void Debugger::removeAllocationsTracking(GlobalObject& global) {
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) {

Просмотреть файл

@ -87,7 +87,6 @@ enum class ResumeMode {
Return,
};
typedef HashSet<WeakHeapPtrGlobalObject,
MovableCellHasher<WeakHeapPtrGlobalObject>, ZoneAllocPolicy>
WeakGlobalObjectSet;
@ -391,6 +390,12 @@ class Debugger : private mozilla::LinkedListElement<Debugger> {
#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. */
@ -449,13 +454,6 @@ class Debugger : private mozilla::LinkedListElement<Debugger> {
*/
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

Просмотреть файл

@ -613,6 +613,11 @@ void Realm::clearTables() {
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
@ -623,6 +628,8 @@ void Realm::setAllocationMetadataBuilder(
}
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

Просмотреть файл

@ -605,6 +605,7 @@ class JS::Realm : public JS::shadow::Realm {
const void* addressOfMetadataBuilder() const {
return &allocationMetadataBuilder_;
}
bool isRecordingAllocations();
void setAllocationMetadataBuilder(
const js::AllocationMetadataBuilder* builder);
void forgetAllocationMetadataBuilder();
@ -797,8 +798,9 @@ class JS::Realm : public JS::shadow::Realm {
// 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);
}

Просмотреть файл

@ -847,7 +847,62 @@ JS_FRIEND_API bool JS::IsProfilingEnabledForContext(JSContext* 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();
}
}

Просмотреть файл

@ -29,6 +29,7 @@
#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
@ -524,6 +525,11 @@ struct JSRuntime : public js::MallocProvider<JSRuntime> {
// 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_;
@ -540,6 +546,11 @@ struct JSRuntime : public js::MallocProvider<JSRuntime> {
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;

Просмотреть файл

@ -1798,6 +1798,16 @@ bool SavedStacks::getLocation(JSContext* cx, const FrameIter& iter,
}
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();
@ -1829,6 +1839,10 @@ void SavedStacks::chooseSamplingProbability(Realm* realm) {
}
MOZ_ASSERT(foundAnyDebuggers);
this->setSamplingProbability(probability);
}
void SavedStacks::setSamplingProbability(double probability) {
if (!bernoulliSeeded) {
mozilla::Array<uint64_t, 2> seed;
GenerateXorShift128PlusSeed(seed);
@ -1859,6 +1873,29 @@ JSObject* SavedStacks::MetadataBuilder::build(
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;
}

Просмотреть файл

@ -227,6 +227,7 @@ class SavedStacks {
Handle<SavedFrame::Lookup> lookup);
SavedFrame* createFrameFromLookup(JSContext* cx,
Handle<SavedFrame::Lookup> lookup);
void setSamplingProbability(double probability);
// Cache for memoizing PCToLineNumber lookups.

Просмотреть файл

@ -524,5 +524,22 @@ void SetConstructUbiNodeForDOMObjectCallback(JSContext* cx,
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