blob: 0ab5e0508c0298d82ec4c08404ba586386271287 [file] [log] [blame]
/* -*- 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/. */
#ifndef vm_Debugger_h
#define vm_Debugger_h
#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"
#include "jscompartment.h"
#include "jsweakmap.h"
#include "jswrapper.h"
#include "ds/TraceableFifo.h"
#include "gc/Barrier.h"
#include "js/Debug.h"
#include "js/HashTable.h"
#include "vm/GlobalObject.h"
#include "vm/SavedStacks.h"
enum JSTrapStatus {
JSTRAP_ERROR,
JSTRAP_CONTINUE,
JSTRAP_RETURN,
JSTRAP_THROW,
JSTRAP_LIMIT
};
namespace js {
class LSprinter;
class Breakpoint;
class DebuggerMemory;
typedef HashSet<ReadBarrieredGlobalObject,
MovableCellHasher<ReadBarrieredGlobalObject>,
SystemAllocPolicy> WeakGlobalObjectSet;
/*
* A weakmap from GC thing keys to JSObject values that supports the keys being
* in different compartments to the values. All values must be in the same
* compartment.
*
* The purpose of this is to allow the garbage collector to easily find edges
* from debuggee object compartments to debugger compartments when calculating
* the compartment groups. Note that these edges are the inverse of the edges
* stored in the cross compartment map.
*
* The current implementation results in all debuggee object compartments being
* swept in the same group as the debugger. This is a conservative approach,
* and compartments may be unnecessarily grouped, however it results in a
* simpler and faster implementation.
*
* If InvisibleKeysOk is true, then the map can have keys in invisible-to-
* debugger compartments. If it is false, we assert that such entries are never
* created.
*
* Also note that keys in these weakmaps can be in any compartment, debuggee or
* not, because they cannot be deleted when a compartment is no longer a
* debuggee: the values need to maintain object identity across add/remove/add
* transitions.
*/
template <class UnbarrieredKey, bool InvisibleKeysOk=false>
class DebuggerWeakMap : private WeakMap<RelocatablePtr<UnbarrieredKey>, RelocatablePtrObject,
MovableCellHasher<RelocatablePtr<UnbarrieredKey>>>
{
private:
typedef RelocatablePtr<UnbarrieredKey> Key;
typedef RelocatablePtrObject Value;
typedef HashMap<JS::Zone*,
uintptr_t,
DefaultHasher<JS::Zone*>,
RuntimeAllocPolicy> CountMap;
CountMap zoneCounts;
JSCompartment* compartment;
public:
typedef WeakMap<Key, Value, MovableCellHasher<Key>> Base;
explicit DebuggerWeakMap(JSContext* cx)
: Base(cx),
zoneCounts(cx->runtime()),
compartment(cx->compartment())
{ }
public:
/* Expose those parts of HashMap public interface that are used by Debugger methods. */
typedef typename Base::Entry Entry;
typedef typename Base::Ptr Ptr;
typedef typename Base::AddPtr AddPtr;
typedef typename Base::Range Range;
typedef typename Base::Enum Enum;
typedef typename Base::Lookup Lookup;
/* Expose WeakMap public interface */
using Base::lookupForAdd;
using Base::all;
using Base::trace;
bool init(uint32_t len = 16) {
return Base::init(len) && zoneCounts.init();
}
template<typename KeyInput, typename ValueInput>
bool relookupOrAdd(AddPtr& p, const KeyInput& k, const ValueInput& v) {
MOZ_ASSERT(v->compartment() == this->compartment);
MOZ_ASSERT(!k->compartment()->options_.mergeable());
MOZ_ASSERT_IF(!InvisibleKeysOk, !k->compartment()->options_.invisibleToDebugger());
MOZ_ASSERT(!Base::has(k));
if (!incZoneCount(k->zone()))
return false;
bool ok = Base::relookupOrAdd(p, k, v);
if (!ok)
decZoneCount(k->zone());
return ok;
}
void remove(const Lookup& l) {
MOZ_ASSERT(Base::has(l));
Base::remove(l);
decZoneCount(l->zone());
}
public:
template <void (traceValueEdges)(JSTracer*, JSObject*)>
void markCrossCompartmentEdges(JSTracer* tracer) {
for (Enum e(*static_cast<Base*>(this)); !e.empty(); e.popFront()) {
traceValueEdges(tracer, e.front().value());
Key key = e.front().key();
TraceEdge(tracer, &key, "Debugger WeakMap key");
if (key != e.front().key())
e.rekeyFront(key);
key.unsafeSet(nullptr);
}
}
bool hasKeyInZone(JS::Zone* zone) {
CountMap::Ptr p = zoneCounts.lookup(zone);
MOZ_ASSERT_IF(p.found(), p->value() > 0);
return p.found();
}
private:
/* Override sweep method to also update our edge cache. */
void sweep() {
for (Enum e(*static_cast<Base*>(this)); !e.empty(); e.popFront()) {
if (gc::IsAboutToBeFinalized(&e.front().mutableKey())) {
decZoneCount(e.front().key()->zone());
e.removeFront();
}
}
Base::assertEntriesNotAboutToBeFinalized();
}
bool incZoneCount(JS::Zone* zone) {
CountMap::Ptr p = zoneCounts.lookupWithDefault(zone, 0);
if (!p)
return false;
++p->value();
return true;
}
void decZoneCount(JS::Zone* zone) {
CountMap::Ptr p = zoneCounts.lookup(zone);
MOZ_ASSERT(p);
MOZ_ASSERT(p->value() > 0);
--p->value();
if (p->value() == 0)
zoneCounts.remove(zone);
}
};
/*
* Env is the type of what ES5 calls "lexical environments" (runtime
* activations of lexical scopes). This is currently just JSObject, and is
* implemented by Call, Block, With, and DeclEnv objects, among others--but
* environments and objects are really two different concepts.
*/
typedef JSObject Env;
class Debugger : private mozilla::LinkedListElement<Debugger>
{
friend class Breakpoint;
friend class DebuggerMemory;
friend class SavedStacks;
friend class mozilla::LinkedListElement<Debugger>;
friend class mozilla::LinkedList<Debugger>;
friend bool (::JS_DefineDebuggerObject)(JSContext* cx, JS::HandleObject obj);
friend bool (::JS::dbg::IsDebugger)(JSObject&);
friend bool (::JS::dbg::GetDebuggeeGlobals)(JSContext*, JSObject&, AutoObjectVector&);
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 {
OnDebuggerStatement,
OnExceptionUnwind,
OnNewScript,
OnEnterFrame,
OnNewGlobalObject,
OnNewPromise,
OnPromiseSettled,
OnGarbageCollection,
OnIonCompilation,
HookCount
};
enum {
JSSLOT_DEBUG_PROTO_START,
JSSLOT_DEBUG_FRAME_PROTO = JSSLOT_DEBUG_PROTO_START,
JSSLOT_DEBUG_ENV_PROTO,
JSSLOT_DEBUG_OBJECT_PROTO,
JSSLOT_DEBUG_SCRIPT_PROTO,
JSSLOT_DEBUG_SOURCE_PROTO,
JSSLOT_DEBUG_MEMORY_PROTO,
JSSLOT_DEBUG_PROTO_STOP,
JSSLOT_DEBUG_HOOK_START = JSSLOT_DEBUG_PROTO_STOP,
JSSLOT_DEBUG_HOOK_STOP = JSSLOT_DEBUG_HOOK_START + HookCount,
JSSLOT_DEBUG_MEMORY_INSTANCE = JSSLOT_DEBUG_HOOK_STOP,
JSSLOT_DEBUG_COUNT
};
class ExecutionObservableSet
{
public:
typedef HashSet<Zone*>::Range ZoneRange;
virtual Zone* singleZone() const { return nullptr; }
virtual JSScript* singleScriptForZoneInvalidation() const { return nullptr; }
virtual const HashSet<Zone*>* zones() const { return nullptr; }
virtual bool shouldRecompileOrInvalidate(JSScript* script) const = 0;
virtual bool shouldMarkAsDebuggee(ScriptFrameIter& iter) const = 0;
};
// This enum is converted to and compare with bool values; NotObserving
// must be 0 and Observing must be 1.
enum IsObserving {
NotObserving = 0,
Observing = 1
};
// Return true if the given compartment is a debuggee of this debugger,
// false otherwise.
bool isDebuggeeUnbarriered(const JSCompartment* compartment) const;
// 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);
}
bool isTrackingTenurePromotions() const {
return trackingTenurePromotions;
}
bool isEnabled() const {
return enabled;
}
void logTenurePromotion(JSRuntime* rt, JSObject& obj, double when);
static SavedFrame* getObjectAllocationSite(JSObject& obj);
struct TenurePromotionsLogEntry : public JS::Traceable
{
TenurePromotionsLogEntry(JSRuntime* rt, JSObject& obj, double when);
const char* className;
double when;
RelocatablePtrObject frame;
size_t size;
static void trace(TenurePromotionsLogEntry* e, JSTracer* trc) {
if (e->frame)
TraceEdge(trc, &e->frame, "Debugger::TenurePromotionsLogEntry::frame");
}
};
struct AllocationsLogEntry : public JS::Traceable
{
AllocationsLogEntry(HandleObject frame, double when, const char* className,
HandleAtom ctorName, size_t size, bool inNursery)
: frame(frame),
when(when),
className(className),
ctorName(ctorName),
size(size),
inNursery(inNursery)
{
MOZ_ASSERT_IF(frame, UncheckedUnwrap(frame)->is<SavedFrame>());
};
RelocatablePtrObject frame;
double when;
const char* className;
RelocatablePtrAtom ctorName;
size_t size;
bool inNursery;
static void trace(AllocationsLogEntry* e, JSTracer* trc) {
if (e->frame)
TraceEdge(trc, &e->frame, "Debugger::AllocationsLogEntry::frame");
if (e->ctorName)
TraceEdge(trc, &e->ctorName, "Debugger::AllocationsLogEntry::ctorName");
}
};
private:
HeapPtrNativeObject 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::HeapPtrObject uncaughtExceptionHook; /* Strong reference. */
bool enabled;
bool allowUnobservedAsmJS;
// Wether to enable code coverage on the Debuggee.
bool collectCoverageInfo;
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;
using TenurePromotionsLog = js::TraceableFifo<TenurePromotionsLogEntry>;
TenurePromotionsLog tenurePromotionsLog;
bool trackingTenurePromotions;
size_t maxTenurePromotionsLogLength;
bool tenurePromotionsLogOverflowed;
using AllocationsLog = js::TraceableFifo<AllocationsLogEntry>;
AllocationsLog allocationsLog;
bool trackingAllocationSites;
double allocationSamplingProbability;
size_t maxAllocationsLogLength;
bool allocationsLogOverflowed;
static const size_t DEFAULT_MAX_LOG_LENGTH = 5000;
bool appendAllocationSite(JSContext* cx, HandleObject obj, HandleSavedFrame frame,
double when);
/*
* Recompute the set of debuggee zones based on the set of debuggee globals.
*/
void recomputeDebuggeeZoneSet();
/*
* 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 bool addAllocationsTracking(JSContext* cx, Handle<GlobalObject*> debuggee);
/*
* Remove allocations tracking for objects allocated within the given
* global's compartment. This is a no-op if there are still Debuggers
* observing this global and who are tracking allocations.
*/
static void removeAllocationsTracking(GlobalObject& global);
/*
* Add or remove allocations tracking for all debuggees.
*/
bool addAllocationsTrackingForAllDebuggees(JSContext* cx);
void removeAllocationsTrackingForAllDebuggees();
/*
* If this Debugger is enabled, and has a onNewGlobalObject handler, then
* this link is inserted into the circular list headed by
* JSRuntime::onNewGlobalObjectWatchers. Otherwise, this is set to a
* singleton cycle.
*/
JSCList onNewGlobalObjectWatchersLink;
/*
* Map from stack frames that are currently on the stack to Debugger.Frame
* instances.
*
* The keys are always live stack frames. We drop them from this map as
* soon as they leave the stack (see slowPathOnLeaveFrame) and in
* removeDebuggee.
*
* We don't trace the keys of this map (the frames are on the stack and
* thus necessarily live), but we do trace the values. It's like a WeakMap
* that way, but since stack frames are not gc-things, the implementation
* has to be different.
*/
typedef HashMap<AbstractFramePtr,
RelocatablePtrNativeObject,
DefaultHasher<AbstractFramePtr>,
RuntimeAllocPolicy> FrameMap;
FrameMap frames;
/* An ephemeral map from JSScript* to Debugger.Script instances. */
typedef DebuggerWeakMap<JSScript*> ScriptWeakMap;
ScriptWeakMap scripts;
/* The map from debuggee source script objects to their Debugger.Source instances. */
typedef DebuggerWeakMap<JSObject*, true> SourceWeakMap;
SourceWeakMap sources;
/* The map from debuggee objects to their Debugger.Object instances. */
typedef DebuggerWeakMap<JSObject*> ObjectWeakMap;
ObjectWeakMap objects;
/* The map from debuggee Envs to Debugger.Environment instances. */
ObjectWeakMap environments;
/*
* Keep track of tracelogger last drained identifiers to know if there are
* lost events.
*/
#ifdef NIGHTLY_BUILD
uint32_t traceLoggerLastDrainedSize;
uint32_t traceLoggerLastDrainedIteration;
#endif
uint32_t traceLoggerScriptedCallsLastDrainedSize;
uint32_t traceLoggerScriptedCallsLastDrainedIteration;
class FrameRange;
class ScriptQuery;
class ObjectQuery;
bool addDebuggeeGlobal(JSContext* cx, Handle<GlobalObject*> obj);
void removeDebuggeeGlobal(FreeOp* fop, GlobalObject* global,
WeakGlobalObjectSet::Enum* debugEnum);
/*
* Cope with an error or exception in a debugger hook.
*
* If callHook is true, then call the uncaughtExceptionHook, if any. If, in
* addition, vp is given, then parse the value returned by
* uncaughtExceptionHook as a resumption value.
*
* If there is no uncaughtExceptionHook, or if it fails, report and clear
* the pending exception on ac.context and return JSTRAP_ERROR.
*
* This always calls ac.leave(); ac is a parameter because this method must
* do some things in the debugger compartment and some things in the
* debuggee compartment.
*/
JSTrapStatus handleUncaughtException(mozilla::Maybe<AutoCompartment>& ac, bool callHook);
JSTrapStatus handleUncaughtException(mozilla::Maybe<AutoCompartment>& ac, MutableHandleValue vp, bool callHook);
JSTrapStatus handleUncaughtExceptionHelper(mozilla::Maybe<AutoCompartment>& ac,
MutableHandleValue* vp, bool callHook);
/*
* Handle the result of a hook that is expected to return a resumption
* value <https://wiki.mozilla.org/Debugger#Resumption_Values>. This is called
* when we return from a debugging hook to debuggee code. The interpreter wants
* a (JSTrapStatus, Value) pair telling it how to proceed.
*
* Precondition: ac is entered. We are in the debugger compartment.
*
* Postcondition: This called ac.leave(). See handleUncaughtException.
*
* If ok is false, the hook failed. If an exception is pending in
* ac.context(), return handleUncaughtException(ac, vp, callhook).
* Otherwise just return JSTRAP_ERROR.
*
* If ok is true, there must be no exception pending in ac.context(). rv may be:
* undefined - Return JSTRAP_CONTINUE to continue execution normally.
* {return: value} or {throw: value} - Call unwrapDebuggeeValue to
* unwrap value. Store the result in *vp and return JSTRAP_RETURN
* or JSTRAP_THROW. The interpreter will force the current frame to
* return or throw an exception.
* null - Return JSTRAP_ERROR to terminate the debuggee with an
* uncatchable error.
* anything else - Make a new TypeError the pending exception and
* return handleUncaughtException(ac, vp, callHook).
*/
JSTrapStatus parseResumptionValue(mozilla::Maybe<AutoCompartment>& ac, bool ok, const Value& rv,
MutableHandleValue vp, bool callHook = true);
GlobalObject* unwrapDebuggeeArgument(JSContext* cx, const Value& v);
static void traceObject(JSTracer* trc, JSObject* obj);
void trace(JSTracer* trc);
static void finalize(FreeOp* fop, JSObject* obj);
void markCrossCompartmentEdges(JSTracer* tracer);
static const Class jsclass;
static bool getHookImpl(JSContext* cx, CallArgs& args, Debugger& dbg, Hook which);
static bool setHookImpl(JSContext* cx, CallArgs& args, Debugger& dbg, Hook which);
static Debugger* fromThisValue(JSContext* cx, const CallArgs& ca, const char* fnname);
static bool getEnabled(JSContext* cx, unsigned argc, Value* vp);
static bool setEnabled(JSContext* cx, unsigned argc, Value* vp);
static bool getOnDebuggerStatement(JSContext* cx, unsigned argc, Value* vp);
static bool setOnDebuggerStatement(JSContext* cx, unsigned argc, Value* vp);
static bool getOnExceptionUnwind(JSContext* cx, unsigned argc, Value* vp);
static bool setOnExceptionUnwind(JSContext* cx, unsigned argc, Value* vp);
static bool getOnNewScript(JSContext* cx, unsigned argc, Value* vp);
static bool setOnNewScript(JSContext* cx, unsigned argc, Value* vp);
static bool getOnEnterFrame(JSContext* cx, unsigned argc, Value* vp);
static bool setOnEnterFrame(JSContext* cx, unsigned argc, Value* vp);
static bool getOnNewGlobalObject(JSContext* cx, unsigned argc, Value* vp);
static bool setOnNewGlobalObject(JSContext* cx, unsigned argc, Value* vp);
static bool getOnNewPromise(JSContext* cx, unsigned argc, Value* vp);
static bool setOnNewPromise(JSContext* cx, unsigned argc, Value* vp);
static bool getOnPromiseSettled(JSContext* cx, unsigned argc, Value* vp);
static bool setOnPromiseSettled(JSContext* cx, unsigned argc, Value* vp);
static bool getUncaughtExceptionHook(JSContext* cx, unsigned argc, Value* vp);
static bool setUncaughtExceptionHook(JSContext* cx, unsigned argc, Value* vp);
static bool getAllowUnobservedAsmJS(JSContext* cx, unsigned argc, Value* vp);
static bool setAllowUnobservedAsmJS(JSContext* cx, unsigned argc, Value* vp);
static bool getCollectCoverageInfo(JSContext* cx, unsigned argc, Value* vp);
static bool setCollectCoverageInfo(JSContext* cx, unsigned argc, Value* vp);
static bool getMemory(JSContext* cx, unsigned argc, Value* vp);
static bool getOnIonCompilation(JSContext* cx, unsigned argc, Value* vp);
static bool setOnIonCompilation(JSContext* cx, unsigned argc, Value* vp);
static bool addDebuggee(JSContext* cx, unsigned argc, Value* vp);
static bool addAllGlobalsAsDebuggees(JSContext* cx, unsigned argc, Value* vp);
static bool removeDebuggee(JSContext* cx, unsigned argc, Value* vp);
static bool removeAllDebuggees(JSContext* cx, unsigned argc, Value* vp);
static bool hasDebuggee(JSContext* cx, unsigned argc, Value* vp);
static bool getDebuggees(JSContext* cx, unsigned argc, Value* vp);
static bool getNewestFrame(JSContext* cx, unsigned argc, Value* vp);
static bool clearAllBreakpoints(JSContext* cx, unsigned argc, Value* vp);
static bool findScripts(JSContext* cx, unsigned argc, Value* vp);
static bool findObjects(JSContext* cx, unsigned argc, Value* vp);
static bool findAllGlobals(JSContext* cx, unsigned argc, Value* vp);
static bool makeGlobalObjectReference(JSContext* cx, unsigned argc, Value* vp);
static bool setupTraceLoggerScriptCalls(JSContext* cx, unsigned argc, Value* vp);
static bool drainTraceLoggerScriptCalls(JSContext* cx, unsigned argc, Value* vp);
static bool startTraceLogger(JSContext* cx, unsigned argc, Value* vp);
static bool endTraceLogger(JSContext* cx, unsigned argc, Value* vp);
#ifdef NIGHTLY_BUILD
static bool setupTraceLogger(JSContext* cx, unsigned argc, Value* vp);
static bool drainTraceLogger(JSContext* cx, unsigned argc, Value* vp);
#endif
static bool construct(JSContext* cx, unsigned argc, Value* vp);
static const JSPropertySpec properties[];
static const JSFunctionSpec methods[];
static void removeFromFrameMapsAndClearBreakpointsIn(JSContext* cx, AbstractFramePtr frame);
static bool updateExecutionObservabilityOfFrames(JSContext* cx, const ExecutionObservableSet& obs,
IsObserving observing);
static bool updateExecutionObservabilityOfScripts(JSContext* cx, const ExecutionObservableSet& obs,
IsObserving observing);
static bool updateExecutionObservability(JSContext* cx, ExecutionObservableSet& obs,
IsObserving observing);
public:
static bool ensureExecutionObservabilityOfOsrFrame(JSContext* cx, InterpreterFrame* frame);
// Public for DebuggerScript_setBreakpoint.
static bool ensureExecutionObservabilityOfScript(JSContext* cx, JSScript* script);
// Whether the Debugger instance needs to observe all non-AOT JS
// execution of its debugees.
IsObserving observesAllExecution() const;
// Whether the Debugger instance needs to observe AOT-compiled asm.js
// execution of its debuggees.
IsObserving observesAsmJS() const;
// Whether the Debugger instance needs to observe coverage of any JavaScript
// execution.
IsObserving observesCoverage() const;
private:
static bool ensureExecutionObservabilityOfFrame(JSContext* cx, AbstractFramePtr frame);
static bool ensureExecutionObservabilityOfCompartment(JSContext* cx, JSCompartment* comp);
static bool hookObservesAllExecution(Hook which);
bool updateObservesAllExecutionOnDebuggees(JSContext* cx, IsObserving observing);
bool updateObservesCoverageOnDebuggees(JSContext* cx, IsObserving observing);
void updateObservesAsmJSOnDebuggees(IsObserving observing);
JSObject* getHook(Hook hook) const;
bool hasAnyLiveHooks() const;
static JSTrapStatus slowPathOnEnterFrame(JSContext* cx, AbstractFramePtr frame);
static bool slowPathOnLeaveFrame(JSContext* cx, AbstractFramePtr frame, bool ok);
static JSTrapStatus slowPathOnDebuggerStatement(JSContext* cx, AbstractFramePtr frame);
static JSTrapStatus slowPathOnExceptionUnwind(JSContext* cx, AbstractFramePtr frame);
static void slowPathOnNewScript(JSContext* cx, HandleScript script);
static void slowPathOnNewGlobalObject(JSContext* cx, Handle<GlobalObject*> global);
static bool slowPathOnLogAllocationSite(JSContext* cx, HandleObject obj, HandleSavedFrame frame,
double when, GlobalObject::DebuggerVector& dbgs);
static void slowPathPromiseHook(JSContext* cx, Hook hook, HandleObject promise);
static void slowPathOnIonCompilation(JSContext* cx, Handle<ScriptVector> scripts,
LSprinter& graph);
template <typename HookIsEnabledFun /* bool (Debugger*) */,
typename FireHookFun /* JSTrapStatus (Debugger*) */>
static JSTrapStatus dispatchHook(JSContext* cx, HookIsEnabledFun hookIsEnabled,
FireHookFun fireHook);
JSTrapStatus fireDebuggerStatement(JSContext* cx, MutableHandleValue vp);
JSTrapStatus fireExceptionUnwind(JSContext* cx, MutableHandleValue vp);
JSTrapStatus fireEnterFrame(JSContext* cx, AbstractFramePtr frame, MutableHandleValue vp);
JSTrapStatus fireNewGlobalObject(JSContext* cx, Handle<GlobalObject*> global, MutableHandleValue vp);
JSTrapStatus firePromiseHook(JSContext* cx, Hook hook, HandleObject promise, MutableHandleValue vp);
/*
* Allocate and initialize a Debugger.Script instance whose referent is
* |script|.
*/
JSObject* newDebuggerScript(JSContext* cx, HandleScript script);
/*
* Allocate and initialize a Debugger.Source instance whose referent is
* |source|.
*/
JSObject* newDebuggerSource(JSContext* cx, js::HandleScriptSource source);
/*
* Receive a "new script" event from the engine. A new script was compiled
* or deserialized.
*/
void fireNewScript(JSContext* cx, HandleScript script);
/*
* Receive a "garbage collection" event from the engine. A GC cycle with the
* given data was recently completed.
*/
void fireOnGarbageCollectionHook(JSContext* cx,
const JS::dbg::GarbageCollectionEvent::Ptr& gcData);
/*
* Receive a "Ion compilation" event from the engine. An Ion compilation with
* the given summary just got linked.
*/
JSTrapStatus fireOnIonCompilationHook(JSContext* cx, Handle<ScriptVector> scripts,
LSprinter& graph);
/*
* Gets a Debugger.Frame object. If maybeIter is non-null, we eagerly copy
* its data if we need to make a new Debugger.Frame.
*/
bool getScriptFrameWithIter(JSContext* cx, AbstractFramePtr frame,
const ScriptFrameIter* maybeIter, MutableHandleValue vp);
inline Breakpoint* firstBreakpoint() const;
static inline Debugger* fromOnNewGlobalObjectWatchersLink(JSCList* link);
static bool replaceFrameGuts(JSContext* cx, AbstractFramePtr from, AbstractFramePtr to,
ScriptFrameIter& iter);
public:
Debugger(JSContext* cx, NativeObject* dbg);
~Debugger();
bool init(JSContext* cx);
inline const js::HeapPtrNativeObject& toJSObject() const;
inline js::HeapPtrNativeObject& toJSObjectRef();
static inline Debugger* fromJSObject(const JSObject* obj);
static Debugger* fromChildJSObject(JSObject* obj);
bool hasMemory() const;
DebuggerMemory& memory() const;
WeakGlobalObjectSet::Range allDebuggees() const { return debuggees.all(); }
/*********************************** Methods for interaction with the GC. */
/*
* A Debugger object is live if:
* * the Debugger JSObject is live (Debugger::trace handles this case); OR
* * it is in the middle of dispatching an event (the event dispatching
* code roots it in this case); OR
* * it is enabled, and it is debugging at least one live compartment,
* and at least one of the following is true:
* - it has a debugger hook installed
* - it has a breakpoint set on a live script
* - it has a watchpoint set on a live object.
*
* Debugger::markAllIteratively handles the last case. If it finds any
* Debugger objects that are definitely live but not yet marked, it marks
* them and returns true. If not, it returns false.
*/
static void markIncomingCrossCompartmentEdges(JSTracer* tracer);
static bool markAllIteratively(GCMarker* trc);
static void markAll(JSTracer* trc);
static void sweepAll(FreeOp* fop);
static void detachAllDebuggersFromGlobal(FreeOp* fop, GlobalObject* global);
static void findZoneEdges(JS::Zone* v, gc::ComponentFinder<JS::Zone>& finder);
/*
* JSTrapStatus Overview
* ---------------------
*
* The |onEnterFrame|, |onDebuggerStatement|, and |onExceptionUnwind|
* methods below return a JSTrapStatus code that indicates how execution
* should proceed:
*
* - JSTRAP_CONTINUE: Continue execution normally.
*
* - JSTRAP_THROW: Throw an exception. The method has set |cx|'s
* pending exception to the value to be thrown.
*
* - JSTRAP_ERROR: Terminate execution (as is done when a script is terminated
* for running too long). The method has cleared |cx|'s pending
* exception.
*
* - JSTRAP_RETURN: Return from the new frame immediately. The method has
* set the youngest JS frame's return value appropriately.
*/
/*
* Announce to the debugger that the context has entered a new JavaScript
* frame, |frame|. Call whatever hooks have been registered to observe new
* frames.
*/
static inline JSTrapStatus onEnterFrame(JSContext* cx, AbstractFramePtr frame);
/*
* Announce to the debugger a |debugger;| statement on has been
* encountered on the youngest JS frame on |cx|. Call whatever hooks have
* been registered to observe this.
*
* Note that this method is called for all |debugger;| statements,
* regardless of the frame's debuggee-ness.
*/
static inline JSTrapStatus onDebuggerStatement(JSContext* cx, AbstractFramePtr frame);
/*
* Announce to the debugger that an exception has been thrown and propagated
* to |frame|. Call whatever hooks have been registered to observe this.
*/
static inline JSTrapStatus onExceptionUnwind(JSContext* cx, AbstractFramePtr frame);
/*
* Announce to the debugger that the thread has exited a JavaScript frame, |frame|.
* If |ok| is true, the frame is returning normally; if |ok| is false, the frame
* is throwing an exception or terminating.
*
* Change cx's current exception and |frame|'s return value to reflect the changes
* in behavior the hooks request, if any. Return the new error/success value.
*
* This function may be called twice for the same outgoing frame; only the
* first call has any effect. (Permitting double calls simplifies some
* cases where an onPop handler's resumption value changes a return to a
* throw, or vice versa: we can redirect to a complete copy of the
* alternative path, containing its own call to onLeaveFrame.)
*/
static inline bool onLeaveFrame(JSContext* cx, AbstractFramePtr frame, bool ok);
static inline void onNewScript(JSContext* cx, HandleScript script);
static inline void onNewGlobalObject(JSContext* cx, Handle<GlobalObject*> global);
static inline bool onLogAllocationSite(JSContext* cx, JSObject* obj, HandleSavedFrame frame,
double when);
static inline bool observesIonCompilation(JSContext* cx);
static inline void onIonCompilation(JSContext* cx, Handle<ScriptVector> scripts,
LSprinter& graph);
static JSTrapStatus onTrap(JSContext* cx, MutableHandleValue vp);
static JSTrapStatus onSingleStep(JSContext* cx, MutableHandleValue vp);
static bool handleBaselineOsr(JSContext* cx, InterpreterFrame* from, jit::BaselineFrame* to);
static bool handleIonBailout(JSContext* cx, jit::RematerializedFrame* from, jit::BaselineFrame* to);
static void handleUnrecoverableIonBailoutError(JSContext* cx, jit::RematerializedFrame* frame);
static void propagateForcedReturn(JSContext* cx, AbstractFramePtr frame, HandleValue rval);
static bool hasLiveHook(GlobalObject* global, Hook which);
static bool inFrameMaps(AbstractFramePtr frame);
/************************************* Functions for use by Debugger.cpp. */
inline bool observesEnterFrame() const;
inline bool observesNewScript() const;
inline bool observesNewGlobalObject() const;
inline bool observesGlobal(GlobalObject* global) const;
bool observesFrame(AbstractFramePtr frame) const;
bool observesFrame(const ScriptFrameIter& iter) const;
bool observesScript(JSScript* script) const;
/*
* If env is nullptr, call vp->setNull() and return true. Otherwise, find
* or create a Debugger.Environment object for the given Env. On success,
* store the Environment object in *vp and return true.
*/
bool wrapEnvironment(JSContext* cx, Handle<Env*> env, MutableHandleValue vp);
/*
* Like cx->compartment()->wrap(cx, vp), but for the debugger compartment.
*
* Preconditions: *vp is a value from a debuggee compartment; cx is in the
* debugger's compartment.
*
* If *vp is an object, this produces a (new or existing) Debugger.Object
* wrapper for it. Otherwise this is the same as JSCompartment::wrap.
*
* If *vp is a magic JS_OPTIMIZED_OUT value, this produces a plain object
* of the form { optimizedOut: true }.
*
* If *vp is a magic JS_OPTIMIZED_ARGUMENTS value signifying missing
* arguments, this produces a plain object of the form { missingArguments:
* true }.
*
* If *vp is a magic JS_UNINITIALIZED_LEXICAL value signifying an
* unaccessible uninitialized binding, this produces a plain object of the
* form { uninitialized: true }.
*/
bool wrapDebuggeeValue(JSContext* cx, MutableHandleValue vp);
/*
* Unwrap a Debug.Object, without rewrapping it for any particular debuggee
* compartment.
*
* Preconditions: cx is in the debugger compartment. *vp is a value in that
* compartment. (*vp should be a "debuggee value", meaning it is the
* debugger's reflection of a value in the debuggee.)
*
* If *vp is a Debugger.Object, store the referent in *vp. Otherwise, if *vp
* is an object, throw a TypeError, because it is not a debuggee
* value. Otherwise *vp is a primitive, so leave it alone.
*
* When passing values from the debuggee to the debugger:
* enter debugger compartment;
* call wrapDebuggeeValue; // compartment- and debugger-wrapping
*
* When passing values from the debugger to the debuggee:
* call unwrapDebuggeeValue; // debugger-unwrapping
* enter debuggee compartment;
* call cx->compartment()->wrap; // compartment-rewrapping
*
* (Extreme nerd sidebar: Unwrapping happens in two steps because there are
* two different kinds of symmetry at work: regardless of which direction
* we're going, we want any exceptions to be created and thrown in the
* debugger compartment--mirror symmetry. But compartment wrapping always
* happens in the target compartment--rotational symmetry.)
*/
bool unwrapDebuggeeValue(JSContext* cx, MutableHandleValue vp);
bool unwrapDebuggeeObject(JSContext* cx, MutableHandleObject obj);
bool unwrapPropertyDescriptor(JSContext* cx, HandleObject obj,
MutableHandle<PropertyDescriptor> desc);
/*
* Store the Debugger.Frame object for frame in *vp.
*
* Use this if you have already access to a frame pointer without having
* to incur the cost of walking the stack.
*/
bool getScriptFrame(JSContext* cx, AbstractFramePtr frame, MutableHandleValue vp) {
return getScriptFrameWithIter(cx, frame, nullptr, vp);
}
/*
* Store the Debugger.Frame object for iter in *vp. Eagerly copies a
* ScriptFrameIter::Data.
*
* Use this if you had to make a ScriptFrameIter to get the required
* frame, in which case the cost of walking the stack has already been
* paid.
*/
bool getScriptFrame(JSContext* cx, const ScriptFrameIter& iter, MutableHandleValue vp) {
return getScriptFrameWithIter(cx, iter.abstractFramePtr(), &iter, vp);
}
/*
* Set |*status| and |*value| to a (JSTrapStatus, Value) pair reflecting a
* standard SpiderMonkey call state: a boolean success value |ok|, a return
* value |rv|, and a context |cx| that may or may not have an exception set.
* If an exception was pending on |cx|, it is cleared (and |ok| is asserted
* to be false).
*/
static void resultToCompletion(JSContext* cx, bool ok, const Value& rv,
JSTrapStatus* status, MutableHandleValue value);
/*
* Set |*result| to a JavaScript completion value corresponding to |status|
* and |value|. |value| should be the return value or exception value, not
* wrapped as a debuggee value. |cx| must be in the debugger compartment.
*/
bool newCompletionValue(JSContext* cx, JSTrapStatus status, Value value,
MutableHandleValue result);
/*
* Precondition: we are in the debuggee compartment (ac is entered) and ok
* is true if the operation in the debuggee compartment succeeded, false on
* error or exception.
*
* Postcondition: we are in the debugger compartment, having called
* ac.leave() even if an error occurred.
*
* On success, a completion value is in vp and ac.context does not have a
* pending exception. (This ordinarily returns true even if the ok argument
* is false.)
*/
bool receiveCompletionValue(mozilla::Maybe<AutoCompartment>& ac, bool ok,
HandleValue val,
MutableHandleValue vp);
/*
* Return the Debugger.Script object for |script|, or create a new one if
* needed. The context |cx| must be in the debugger compartment; |script|
* must be a script in a debuggee compartment.
*/
JSObject* wrapScript(JSContext* cx, HandleScript script);
/*
* Return the Debugger.Source object for |source|, or create a new one if
* needed. The context |cx| must be in the debugger compartment; |source|
* must be a script source object in a debuggee compartment.
*/
JSObject* wrapSource(JSContext* cx, js::HandleScriptSource source);
private:
Debugger(const Debugger&) = delete;
Debugger & operator=(const Debugger&) = delete;
};
template<>
struct DefaultGCPolicy<Debugger::TenurePromotionsLogEntry> {
static void trace(JSTracer* trc, Debugger::TenurePromotionsLogEntry* e, const char*) {
Debugger::TenurePromotionsLogEntry::trace(e, trc);
}
};
template<>
struct DefaultGCPolicy<Debugger::AllocationsLogEntry> {
static void trace(JSTracer* trc, Debugger::AllocationsLogEntry* e, const char*) {
Debugger::AllocationsLogEntry::trace(e, trc);
}
};
class BreakpointSite {
friend class Breakpoint;
friend struct ::JSCompartment;
friend class ::JSScript;
friend class Debugger;
public:
JSScript* script;
jsbytecode * const pc;
private:
JSCList breakpoints; /* cyclic list of all js::Breakpoints at this instruction */
size_t enabledCount; /* number of breakpoints in the list that are enabled */
void recompile(FreeOp* fop);
public:
BreakpointSite(JSScript* script, jsbytecode* pc);
Breakpoint* firstBreakpoint() const;
bool hasBreakpoint(Breakpoint* bp);
void inc(FreeOp* fop);
void dec(FreeOp* fop);
void destroyIfEmpty(FreeOp* fop);
};
/*
* Each Breakpoint is a member of two linked lists: its debugger's list and its
* site's list.
*
* GC rules:
* - script is live, breakpoint exists, and debugger is enabled
* ==> debugger is live
* - script is live, breakpoint exists, and debugger is live
* ==> retain the breakpoint and the handler object is live
*
* Debugger::markAllIteratively implements these two rules. It uses
* Debugger::hasAnyLiveHooks to check for rule 1.
*
* Nothing else causes a breakpoint to be retained, so if its script or
* debugger is collected, the breakpoint is destroyed during GC sweep phase,
* even if the debugger compartment isn't being GC'd. This is implemented in
* Zone::sweepBreakpoints.
*/
class Breakpoint {
friend struct ::JSCompartment;
friend class Debugger;
public:
Debugger * const debugger;
BreakpointSite * const site;
private:
/* |handler| is marked unconditionally during minor GC. */
js::PreBarrieredObject handler;
JSCList debuggerLinks;
JSCList siteLinks;
public:
static Breakpoint* fromDebuggerLinks(JSCList* links);
static Breakpoint* fromSiteLinks(JSCList* links);
Breakpoint(Debugger* debugger, BreakpointSite* site, JSObject* handler);
void destroy(FreeOp* fop);
Breakpoint* nextInDebugger();
Breakpoint* nextInSite();
const PreBarrieredObject& getHandler() const { return handler; }
PreBarrieredObject& getHandlerRef() { return handler; }
};
Breakpoint*
Debugger::firstBreakpoint() const
{
if (JS_CLIST_IS_EMPTY(&breakpoints))
return nullptr;
return Breakpoint::fromDebuggerLinks(JS_NEXT_LINK(&breakpoints));
}
/* static */ Debugger*
Debugger::fromOnNewGlobalObjectWatchersLink(JSCList* link) {
char* p = reinterpret_cast<char*>(link);
return reinterpret_cast<Debugger*>(p - offsetof(Debugger, onNewGlobalObjectWatchersLink));
}
const js::HeapPtrNativeObject&
Debugger::toJSObject() const
{
MOZ_ASSERT(object);
return object;
}
js::HeapPtrNativeObject&
Debugger::toJSObjectRef()
{
MOZ_ASSERT(object);
return object;
}
bool
Debugger::observesEnterFrame() const
{
return enabled && getHook(OnEnterFrame);
}
bool
Debugger::observesNewScript() const
{
return enabled && getHook(OnNewScript);
}
bool
Debugger::observesNewGlobalObject() const
{
return enabled && getHook(OnNewGlobalObject);
}
bool
Debugger::observesGlobal(GlobalObject* global) const
{
ReadBarriered<GlobalObject*> debuggee(global);
return debuggees.has(debuggee);
}
/* static */ void
Debugger::onNewScript(JSContext* cx, HandleScript script)
{
// We early return in slowPathOnNewScript for self-hosted scripts, so we can
// ignore those in our assertion here.
MOZ_ASSERT_IF(!script->compartment()->options().invisibleToDebugger() &&
!script->selfHosted(),
script->compartment()->firedOnNewGlobalObject);
if (script->compartment()->isDebuggee())
slowPathOnNewScript(cx, script);
}
/* static */ void
Debugger::onNewGlobalObject(JSContext* cx, Handle<GlobalObject*> global)
{
MOZ_ASSERT(!global->compartment()->firedOnNewGlobalObject);
#ifdef DEBUG
global->compartment()->firedOnNewGlobalObject = true;
#endif
if (!JS_CLIST_IS_EMPTY(&cx->runtime()->onNewGlobalObjectWatchers))
Debugger::slowPathOnNewGlobalObject(cx, global);
}
/* static */ bool
Debugger::onLogAllocationSite(JSContext* cx, JSObject* obj, HandleSavedFrame frame, double when)
{
GlobalObject::DebuggerVector* dbgs = cx->global()->getDebuggers();
if (!dbgs || dbgs->empty())
return true;
RootedObject hobj(cx, obj);
return Debugger::slowPathOnLogAllocationSite(cx, hobj, frame, when, *dbgs);
}
bool ReportObjectRequired(JSContext* cx);
} /* namespace js */
#endif /* vm_Debugger_h */