Bug 1150253 - Part 1: SpiderMonkey should call an embedder-provided callback

instead of running the onGarbageCollection hook immediately; r=sfink
This commit is contained in:
Nick Fitzgerald 2015-04-22 09:43:02 -07:00
Родитель ce84bb0e7e
Коммит 1bbdc65eb9
8 изменённых файлов: 262 добавлений и 144 удалений

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

@ -12,10 +12,11 @@
#include "mozilla/Assertions.h"
#include "mozilla/Attributes.h"
#include "mozilla/MemoryReporting.h"
#include "mozilla/Move.h"
#include "mozilla/UniquePtr.h"
#include "jspubtd.h"
#include "js/GCAPI.h"
#include "js/RootingAPI.h"
#include "js/TypeDecls.h"
@ -24,6 +25,9 @@ class Debugger;
}
namespace JS {
using mozilla::UniquePtr;
namespace dbg {
// Helping embedding code build objects for Debugger
@ -261,6 +265,24 @@ class BuilderOrigin : public Builder {
void SetDebuggerMallocSizeOf(JSRuntime* runtime, mozilla::MallocSizeOf mallocSizeOf);
// Debugger and Garbage Collection Events
// --------------------------------------
//
// The Debugger wants to report about its debuggees' GC cycles, however entering
// JS after a GC is troublesome since SpiderMonkey will often do something like
// force a GC and then rely on the nursery being empty. If we call into some
// Debugger's hook after the GC, then JS runs and the nursery won't be
// empty. Instead, we rely on embedders to call back into SpiderMonkey after a
// GC and notify Debuggers to call their onGarbageCollection hook.
// For each Debugger that observed a debuggee involved in the given GC event,
// call its `onGarbageCollection` hook.
JS_PUBLIC_API(bool)
FireOnGarbageCollectionHook(JSContext* cx, GarbageCollectionEvent::Ptr&& data);
// Handlers for observing Promises
// -------------------------------

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

@ -7,12 +7,18 @@
#ifndef js_GCAPI_h
#define js_GCAPI_h
#include "mozilla/UniquePtr.h"
#include "mozilla/Vector.h"
#include "js/HeapAPI.h"
namespace js {
namespace gc {
class GCRuntime;
}
namespace gcstats {
struct Statistics;
}
}
typedef enum JSGCMode {
@ -42,6 +48,8 @@ typedef enum JSGCInvocationKind {
namespace JS {
using mozilla::UniquePtr;
#define GCREASONS(D) \
/* Reasons internal to the JS engine */ \
D(API) \
@ -254,6 +262,56 @@ FinishIncrementalGC(JSRuntime* rt, gcreason::Reason reason);
extern JS_PUBLIC_API(void)
AbortIncrementalGC(JSRuntime* rt);
namespace dbg {
// The `JS::dbg::GarbageCollectionEvent` class is essentially a view of the
// `js::gcstats::Statistics` data without the uber implementation-specific bits.
// It should generally be palatable for web developers.
class GarbageCollectionEvent
{
// The major GC number of the GC cycle this data pertains to.
uint64_t majorGCNumber_;
// Reference to a non-owned, statically allocated C string. This is a very
// short reason explaining why a GC was triggered.
const char* reason;
// Reference to a nullable, non-owned, statically allocated C string. If the
// collection was forced to be non-incremental, this is a short reason of
// why the GC could not perform an incremental collection.
const char* nonincrementalReason;
// Represents a single slice of a possibly multi-slice incremental garbage
// collection.
struct Collection {
int64_t startTimestamp;
int64_t endTimestamp;
};
// The set of garbage collection slices that made up this GC cycle.
mozilla::Vector<Collection> collections;
GarbageCollectionEvent(const GarbageCollectionEvent& rhs) = delete;
GarbageCollectionEvent& operator=(const GarbageCollectionEvent& rhs) = delete;
public:
explicit GarbageCollectionEvent(uint64_t majorGCNum)
: majorGCNumber_(majorGCNum)
, reason(nullptr)
, nonincrementalReason(nullptr)
, collections()
{ }
using Ptr = UniquePtr<GarbageCollectionEvent, DeletePolicy<GarbageCollectionEvent>>;
static Ptr Create(JSRuntime* rt, ::js::gcstats::Statistics& stats, uint64_t majorGCNumber);
JSObject* toJSObject(JSContext* cx) const;
uint64_t majorGCNumber() const { return majorGCNumber_; }
};
} // namespace dbg
enum GCProgress {
/*
* During non-incremental GC, the GC is bracketed by JSGC_CYCLE_BEGIN/END
@ -280,6 +338,8 @@ struct JS_PUBLIC_API(GCDescription) {
char16_t* formatMessage(JSRuntime* rt) const;
char16_t* formatJSON(JSRuntime* rt, uint64_t timestamp) const;
JS::dbg::GarbageCollectionEvent::Ptr toGCEvent(JSRuntime* rt) const;
};
typedef void

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

@ -974,9 +974,6 @@ Statistics::endGC()
if (fp)
printStats();
if (!aborted)
Debugger::onGarbageCollection(runtime, *this);
// Clear the timers at the end of a GC because we accumulate time in
// between GCs for some (which come before PHASE_GC_BEGIN in the list.)
PodZero(&phaseStartTimes[PHASE_GC_BEGIN], PHASE_LIMIT - PHASE_GC_BEGIN);

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

@ -264,7 +264,8 @@ void
Zone::notifyObservingDebuggers()
{
for (CompartmentsInZoneIter comps(this); !comps.done(); comps.next()) {
RootedGlobalObject global(runtimeFromAnyThread(), comps->maybeGlobal());
JSRuntime* rt = runtimeFromAnyThread();
RootedGlobalObject global(rt, comps->maybeGlobal());
if (!global)
continue;
@ -272,8 +273,16 @@ Zone::notifyObservingDebuggers()
if (!dbgs)
continue;
for (GlobalObject::DebuggerVector::Range r = dbgs->all(); !r.empty(); r.popFront())
r.front()->debuggeeIsBeingCollected();
for (GlobalObject::DebuggerVector::Range r = dbgs->all(); !r.empty(); r.popFront()) {
if (!r.front()->debuggeeIsBeingCollected(rt->gc.majorGCCount())) {
#ifdef DEBUG
fprintf(stderr,
"OOM while notifying observing Debuggers of a GC: The onGarbageCollection\n"
"hook will not be fired for this GC for some Debuggers!\n");
#endif
return;
}
}
}
}

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

@ -7029,6 +7029,12 @@ JS::GCDescription::formatMessage(JSRuntime* rt) const
return rt->gc.stats.formatMessage();
}
JS::dbg::GarbageCollectionEvent::Ptr
JS::GCDescription::toGCEvent(JSRuntime* rt) const
{
return JS::dbg::GarbageCollectionEvent::Create(rt, rt->gc.stats, rt->gc.majorGCCount());
}
char16_t*
JS::GCDescription::formatJSON(JSRuntime* rt, uint64_t timestamp) const
{

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

@ -44,6 +44,7 @@ using js::frontend::IsIdentifier;
using mozilla::ArrayLength;
using mozilla::DebugOnly;
using mozilla::Maybe;
using mozilla::UniquePtr;
/*** Forward declarations ************************************************************************/
@ -358,9 +359,8 @@ Debugger::Debugger(JSContext* cx, NativeObject* dbg)
: object(dbg),
uncaughtExceptionHook(nullptr),
enabled(true),
observedGCs(cx),
allowUnobservedAsmJS(false),
debuggeeWasCollected(false),
inOnGCHook(false),
trackingAllocationSites(false),
allocationSamplingProbability(1.0),
allocationsLogLength(0),
@ -408,6 +408,7 @@ Debugger::init(JSContext* cx)
scripts.init() &&
sources.init() &&
objects.init() &&
observedGCs.init() &&
environments.init();
if (!ok)
ReportOutOfMemory(cx);
@ -878,71 +879,6 @@ Debugger::wrapDebuggeeValue(JSContext* cx, MutableHandleValue vp)
return true;
}
JSObject*
Debugger::translateGCStatistics(JSContext* cx, const gcstats::Statistics& stats)
{
// If this functions triggers a GC then the statistics object will change
// underneath us.
gc::AutoSuppressGC suppressGC(cx);
RootedObject obj(cx, NewBuiltinClassInstance<PlainObject>(cx));
if (!obj)
return nullptr;
const char* nonincrementalReason = stats.nonincrementalReason();
RootedValue nonincrementalReasonValue(cx, NullValue());
if (nonincrementalReason) {
JSAtom* atomized = Atomize(cx, nonincrementalReason, strlen(nonincrementalReason));
if (!atomized)
return nullptr;
nonincrementalReasonValue.setString(atomized);
}
if (!DefineProperty(cx, obj, cx->names().nonincrementalReason, nonincrementalReasonValue))
return nullptr;
RootedArrayObject slicesArray(cx, NewDenseEmptyArray(cx));
if (!slicesArray)
return nullptr;
size_t idx = 0;
for (auto range = stats.sliceRange(); !range.empty(); range.popFront()) {
if (idx == 0) {
// There is only one GC reason for the whole cycle, but for legacy
// reasons, this data is stored and replicated on each slice.
const char* reason = gcstats::ExplainReason(range.front().reason);
JSAtom* atomized = Atomize(cx, reason, strlen(reason));
if (!atomized)
return nullptr;
RootedValue reasonVal(cx, StringValue(atomized));
if (!DefineProperty(cx, obj, cx->names().reason, reasonVal))
return nullptr;
}
RootedPlainObject collectionObj(cx, NewBuiltinClassInstance<PlainObject>(cx));
if (!collectionObj)
return nullptr;
RootedValue start(cx, NumberValue(range.front().start));
RootedValue end(cx, NumberValue(range.front().end));
if (!DefineProperty(cx, collectionObj, cx->names().startTimestamp, start) ||
!DefineProperty(cx, collectionObj, cx->names().endTimestamp, end))
{
return nullptr;
}
RootedValue collectionVal(cx, ObjectValue(*collectionObj));
if (!DefineElement(cx, slicesArray, idx++, collectionVal))
return nullptr;
}
RootedValue slicesValue(cx, ObjectValue(*slicesArray));
if (!DefineProperty(cx, obj, cx->names().collections, slicesValue))
return nullptr;
return obj.get();
}
bool
Debugger::unwrapDebuggeeObject(JSContext* cx, MutableHandleObject obj)
{
@ -1334,18 +1270,11 @@ Debugger::fireNewScript(JSContext* cx, HandleScript script)
}
void
Debugger::fireOnGarbageCollectionHook(JSRuntime* rt, const gcstats::Statistics& stats)
Debugger::fireOnGarbageCollectionHook(JSContext* cx,
const JS::dbg::GarbageCollectionEvent::Ptr& gcData)
{
if (inOnGCHook)
return;
AutoOnGCHookReentrancyGuard guard(*this);
MOZ_ASSERT(debuggeeWasCollected);
debuggeeWasCollected = false;
JSContext* cx = DefaultJSContext(rt);
MOZ_ASSERT(cx);
MOZ_ASSERT(observedGC(gcData->majorGCNumber()));
observedGCs.remove(gcData->majorGCNumber());
RootedObject hook(cx, getHook(OnGarbageCollection));
MOZ_ASSERT(hook);
@ -1354,15 +1283,15 @@ Debugger::fireOnGarbageCollectionHook(JSRuntime* rt, const gcstats::Statistics&
Maybe<AutoCompartment> ac;
ac.emplace(cx, object);
JSObject* statsObj = translateGCStatistics(cx, stats);
if (!statsObj) {
JSObject* dataObj = gcData->toJSObject(cx);
if (!dataObj) {
handleUncaughtException(ac, false);
return;
}
RootedValue statsVal(cx, ObjectValue(*statsObj));
RootedValue dataVal(cx, ObjectValue(*dataObj));
RootedValue rv(cx);
if (!Invoke(cx, ObjectValue(*object), ObjectValue(*hook), 1, statsVal.address(), &rv))
if (!Invoke(cx, ObjectValue(*object), ObjectValue(*hook), 1, dataVal.address(), &rv))
handleUncaughtException(ac, true);
}
@ -7865,3 +7794,129 @@ JS::dbg::IsDebugger(JS::Value val)
return js::Debugger::fromJSObject(&obj) != nullptr;
}
/*** JS::dbg::GarbageCollectionEvent **************************************************************/
namespace JS {
namespace dbg {
/* static */ GarbageCollectionEvent::Ptr
GarbageCollectionEvent::Create(JSRuntime* rt, ::js::gcstats::Statistics& stats, uint64_t gcNumber)
{
auto data = rt->make_unique<GarbageCollectionEvent>(gcNumber);
if (!data)
return nullptr;
data->nonincrementalReason = stats.nonincrementalReason();
for (auto range = stats.sliceRange(); !range.empty(); range.popFront()) {
if (!data->reason) {
// There is only one GC reason for the whole cycle, but for legacy
// reasons this data is stored and replicated on each slice. Each
// slice used to have its own GCReason, but now they are all the
// same.
data->reason = gcstats::ExplainReason(range.front().reason);
MOZ_ASSERT(data->reason);
}
if (!data->collections.growBy(1))
return nullptr;
data->collections.back().startTimestamp = range.front().start;
data->collections.back().endTimestamp = range.front().end;
}
return data;
}
static bool
DefineStringProperty(JSContext* cx, HandleObject obj, PropertyName* propName, const char* strVal)
{
RootedValue val(cx, UndefinedValue());
if (strVal) {
JSAtom* atomized = Atomize(cx, strVal, strlen(strVal));
if (!atomized)
return false;
val = StringValue(atomized);
}
return DefineProperty(cx, obj, propName, val);
}
JSObject*
GarbageCollectionEvent::toJSObject(JSContext* cx) const
{
RootedObject obj(cx, NewBuiltinClassInstance<PlainObject>(cx));
if (!obj ||
!DefineStringProperty(cx, obj, cx->names().nonincrementalReason, nonincrementalReason) ||
!DefineStringProperty(cx, obj, cx->names().reason, reason))
{
return nullptr;
}
RootedArrayObject slicesArray(cx, NewDenseEmptyArray(cx));
if (!slicesArray)
return nullptr;
size_t idx = 0;
for (auto range = collections.all(); !range.empty(); range.popFront()) {
RootedPlainObject collectionObj(cx, NewBuiltinClassInstance<PlainObject>(cx));
if (!collectionObj)
return nullptr;
RootedValue start(cx, NumberValue(range.front().startTimestamp));
RootedValue end(cx, NumberValue(range.front().endTimestamp));
if (!DefineProperty(cx, collectionObj, cx->names().startTimestamp, start) ||
!DefineProperty(cx, collectionObj, cx->names().endTimestamp, end))
{
return nullptr;
}
RootedValue collectionVal(cx, ObjectValue(*collectionObj));
if (!DefineElement(cx, slicesArray, idx++, collectionVal))
return nullptr;
}
RootedValue slicesValue(cx, ObjectValue(*slicesArray));
if (!DefineProperty(cx, obj, cx->names().collections, slicesValue))
return nullptr;
return obj;
}
JS_PUBLIC_API(bool)
FireOnGarbageCollectionHook(JSContext* cx, JS::dbg::GarbageCollectionEvent::Ptr&& data)
{
AutoObjectVector triggered(cx);
{
// We had better not GC (and potentially get a dangling Debugger
// pointer) while finding all Debuggers observing a debuggee that
// participated in this GC.
AutoCheckCannotGC noGC;
for (Debugger* dbg = cx->runtime()->debuggerList.getFirst(); dbg; dbg = dbg->getNext()) {
if (dbg->enabled &&
dbg->observedGC(data->majorGCNumber()) &&
dbg->getHook(Debugger::OnGarbageCollection))
{
if (!triggered.append(dbg->object)) {
JS_ReportOutOfMemory(cx);
return false;
}
}
}
}
for ( ; !triggered.empty(); triggered.popBack()) {
Debugger* dbg = Debugger::fromJSObject(triggered.back());
dbg->fireOnGarbageCollectionHook(cx, data);
MOZ_ASSERT(!cx->isExceptionPending());
}
return true;
}
} // namespace dbg
} // namespace JS

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

@ -10,6 +10,8 @@
#include "mozilla/GuardObjects.h"
#include "mozilla/LinkedList.h"
#include "mozilla/Range.h"
#include "mozilla/UniquePtr.h"
#include "mozilla/Vector.h"
#include "jsclist.h"
#include "jscntxt.h"
@ -190,6 +192,8 @@ class Debugger : private mozilla::LinkedListElement<Debugger>
friend JSObject* SavedStacksMetadataCallback(JSContext* cx);
friend void JS::dbg::onNewPromise(JSContext* cx, HandleObject promise);
friend void JS::dbg::onPromiseSettled(JSContext* cx, HandleObject promise);
friend bool JS::dbg::FireOnGarbageCollectionHook(JSContext* cx,
JS::dbg::GarbageCollectionEvent::Ptr&& data);
public:
enum Hook {
@ -242,9 +246,17 @@ class Debugger : private mozilla::LinkedListElement<Debugger>
// false otherwise.
bool isDebuggee(const JSCompartment* compartment) const;
// Notify this Debugger that one of its debuggee compartments' zones is
// being collected.
void debuggeeIsBeingCollected() { debuggeeWasCollected = true; }
// Return true if this Debugger observed a debuggee that participated in the
// GC identified by the given GC number. Return false otherwise.
bool observedGC(uint64_t majorGCNumber) const {
return observedGCs.has(majorGCNumber);
}
// Notify this Debugger that one or more of its debuggees is participating
// in the GC identified by the given GC number.
bool debuggeeIsBeingCollected(uint64_t majorGCNumber) {
return observedGCs.put(majorGCNumber);
}
private:
HeapPtrNativeObject object; /* The Debugger object. Strong reference. */
@ -253,6 +265,10 @@ class Debugger : private mozilla::LinkedListElement<Debugger>
bool enabled;
JSCList breakpoints; /* Circular list of all js::Breakpoints in this debugger */
// The set of GC numbers for which one or more of this Debugger's observed
// debuggees participated in.
js::HashSet<uint64_t> observedGCs;
struct AllocationSite : public mozilla::LinkedListElement<AllocationSite>
{
AllocationSite(HandleObject frame, int64_t when, const char* className)
@ -270,38 +286,6 @@ class Debugger : private mozilla::LinkedListElement<Debugger>
typedef mozilla::LinkedList<AllocationSite> AllocationSiteList;
bool allowUnobservedAsmJS;
// During a GC cycle, this is true if one of this Debugger's debuggees was
// collected. When the GC cycle completes, this flag is reset.
bool debuggeeWasCollected;
// True while we are executing the onGarbageCollection hook, and therefore
// should not fire the hook for this Debugger instance again if there is a
// GC while we are executing the hook. See also
// `AutoOnGCHookReentrancyGuard` below.
bool inOnGCHook;
// RAII class to automatically guard against reentrancy into the
// OnGarbageCollection hook.
class MOZ_STACK_CLASS AutoOnGCHookReentrancyGuard {
MOZ_DECL_USE_GUARD_OBJECT_NOTIFIER;
Debugger& dbg;
public:
explicit AutoOnGCHookReentrancyGuard(Debugger& dbg MOZ_GUARD_OBJECT_NOTIFIER_PARAM)
: dbg(dbg)
{
MOZ_GUARD_OBJECT_NOTIFIER_INIT;
MOZ_ASSERT(!dbg.inOnGCHook);
dbg.inOnGCHook = true;
}
~AutoOnGCHookReentrancyGuard() {
MOZ_ASSERT(dbg.inOnGCHook);
dbg.inOnGCHook = false;
}
};
bool trackingAllocationSites;
double allocationSamplingProbability;
AllocationSiteList allocationsLog;
@ -554,9 +538,10 @@ class Debugger : private mozilla::LinkedListElement<Debugger>
/*
* Receive a "garbage collection" event from the engine. A GC cycle with the
* given statistics was just completed.
* given data was recently completed.
*/
void fireOnGarbageCollectionHook(JSRuntime* rt, const gcstats::Statistics& stats);
void fireOnGarbageCollectionHook(JSContext* cx,
const JS::dbg::GarbageCollectionEvent::Ptr& gcData);
/*
* Gets a Debugger.Frame object. If maybeIter is non-null, we eagerly copy
@ -675,7 +660,6 @@ class Debugger : private mozilla::LinkedListElement<Debugger>
static inline void onNewGlobalObject(JSContext* cx, Handle<GlobalObject*> global);
static inline bool onLogAllocationSite(JSContext* cx, JSObject* obj, HandleSavedFrame frame,
int64_t when);
static inline void onGarbageCollection(JSRuntime* rt, const gcstats::Statistics& stats);
static JSTrapStatus onTrap(JSContext* cx, MutableHandleValue vp);
static JSTrapStatus onSingleStep(JSContext* cx, MutableHandleValue vp);
static bool handleBaselineOsr(JSContext* cx, InterpreterFrame* from, jit::BaselineFrame* to);
@ -724,13 +708,6 @@ class Debugger : private mozilla::LinkedListElement<Debugger>
*/
bool wrapDebuggeeValue(JSContext* cx, MutableHandleValue vp);
/*
* Converts an implementor level of detail gcstats::Statistics object into a
* JSObject that web developers should be able to make sense of. Returns
* nullptr on failure.
*/
JSObject* translateGCStatistics(JSContext* cx, const gcstats::Statistics& stats);
/*
* Unwrap a Debug.Object, without rewrapping it for any particular debuggee
* compartment.
@ -992,18 +969,9 @@ Debugger::onLogAllocationSite(JSContext* cx, JSObject* obj, HandleSavedFrame fra
return Debugger::slowPathOnLogAllocationSite(cx, hobj, frame, when, *dbgs);
}
/* static */ void
Debugger::onGarbageCollection(JSRuntime* rt, const gcstats::Statistics& stats)
{
for (Debugger* dbg = rt->debuggerList.getFirst(); dbg; dbg = dbg->getNext()) {
if (dbg->debuggeeWasCollected && dbg->getHook(OnGarbageCollection)) {
dbg->fireOnGarbageCollectionHook(rt, stats);
}
}
}
bool ReportObjectRequired(JSContext* cx);
} /* namespace js */
#endif /* vm_Debugger_h */

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

@ -31,6 +31,7 @@
#include "gc/GCRuntime.h"
#include "gc/Tracer.h"
#include "irregexp/RegExpStack.h"
#include "js/Debug.h"
#include "js/HashTable.h"
#ifdef DEBUG
# include "js/Proxy.h" // For AutoEnterPolicy