Bug 1001678 - Part 1: Implement an allocation log for Debugger.Memory. r=jimb

This commit is contained in:
Nick Fitzgerald 2014-08-06 16:24:00 -04:00
parent 87acb958cd
commit 8e6cd27902
19 changed files with 465 additions and 15 deletions

View File

@ -0,0 +1,31 @@
// Test basic usage of drainAllocationsLog()
const root = newGlobal();
const dbg = new Debugger();
const wrappedRoot = dbg.addDebuggee(root)
dbg.memory.trackingAllocationSites = true;
root.eval("(" + function immediate() {
this.tests = [
({}),
[],
/(two|2)\s*problems/,
new function Ctor(){},
new Object(),
new Array(),
new Date(),
];
} + "());");
const allocs = dbg.memory.drainAllocationsLog();
print(allocs.join("\n--------------------------------------------------------------------------\n"));
print("Total number of allocations logged: " + allocs.length);
let idx = -1;
for (let object of root.tests) {
let wrappedObject = wrappedRoot.makeDebuggeeValue(object);
let allocSite = wrappedObject.allocationSite;
let newIdx = allocs.indexOf(allocSite);
assertEq(newIdx > idx, true);
idx = newIdx;
}

View File

@ -0,0 +1,14 @@
// Test that drainAllocationsLog fails when we aren't trackingAllocationSites.
load(libdir + 'asserts.js');
const root = newGlobal();
const dbg = new Debugger();
dbg.memory.trackingAllocationSites = true;
root.eval("this.alloc1 = {}");
dbg.memory.trackingAllocationSites = false;
root.eval("this.alloc2 = {};");
assertThrowsInstanceOf(() => dbg.memory.drainAllocationsLog(),
Error);

View File

@ -0,0 +1,24 @@
// Test when there are more allocations than the maximum log length.
const root = newGlobal();
const dbg = new Debugger();
dbg.addDebuggee(root)
dbg.memory.maxAllocationsLogLength = 3;
dbg.memory.trackingAllocationSites = true;
root.eval([
"this.alloc1 = {};", // line 1
"this.alloc2 = {};", // line 2
"this.alloc3 = {};", // line 3
"this.alloc4 = {};", // line 4
].join("\n"));
const allocs = dbg.memory.drainAllocationsLog();
// Should have stayed at the maximum length.
assertEq(allocs.length, 3);
// Should have kept the most recent allocation.
assertEq(allocs[2].line, 4);
// Should have thrown away the oldest allocation.
assertEq(allocs.map(x => x.line).indexOf(1), -1);

View File

@ -0,0 +1,21 @@
// Test that when we shorten the maximum log length, we won't get a longer log
// than that new maximum afterwards.
const root = newGlobal();
const dbg = new Debugger();
dbg.addDebuggee(root)
dbg.memory.trackingAllocationSites = true;
root.eval([
"this.alloc1 = {};", // line 1
"this.alloc2 = {};", // line 2
"this.alloc3 = {};", // line 3
"this.alloc4 = {};", // line 4
].join("\n"));
dbg.memory.maxAllocationsLogLength = 1;
const allocs = dbg.memory.drainAllocationsLog();
// Should have trimmed down to the new maximum length.
assertEq(allocs.length, 1);

View File

@ -0,0 +1,9 @@
// Test an empty allocation log.
const root = newGlobal();
const dbg = new Debugger();
dbg.addDebuggee(root)
dbg.memory.trackingAllocationSites = true;
const allocs = dbg.memory.drainAllocationsLog();
assertEq(allocs.length, 0);

View File

@ -0,0 +1,23 @@
// Test doing a GC while we have a non-empty log.
const root = newGlobal();
const dbg = new Debugger();
dbg.addDebuggee(root)
dbg.memory.trackingAllocationSites = true;
root.eval("(" + function immediate() {
this.tests = [
({}),
[],
/(two|2)\s*problems/,
new function Ctor(){},
new Object(),
new Array(),
new Date(),
];
} + "());");
gc();
const allocs = dbg.memory.drainAllocationsLog();
assertEq(allocs.length >= root.tests.length, true);

View File

@ -0,0 +1,10 @@
// Test retrieving the log when allocation tracking hasn't ever been enabled.
load(libdir + 'asserts.js');
const root = newGlobal();
const dbg = new Debugger();
dbg.addDebuggee(root)
assertThrowsInstanceOf(() => dbg.memory.drainAllocationsLog(),
Error);

View File

@ -0,0 +1,30 @@
// Test retrieving the log multiple times.
const root = newGlobal();
const dbg = new Debugger();
dbg.addDebuggee(root)
root.eval([
"this.allocs = [];",
"this.doFirstAlloc = " + function () {
this.allocs.push({}); this.firstAllocLine = Error().lineNumber;
},
"this.doSecondAlloc = " + function () {
this.allocs.push(new Object()); this.secondAllocLine = Error().lineNumber;
}
].join("\n"));
dbg.memory.trackingAllocationSites = true;
root.doFirstAlloc();
let allocs1 = dbg.memory.drainAllocationsLog();
root.doSecondAlloc();
let allocs2 = dbg.memory.drainAllocationsLog();
let allocs1Lines = allocs1.map(x => x.line);
assertEq(allocs1Lines.indexOf(root.firstAllocLine) != -1, true);
assertEq(allocs1Lines.indexOf(root.secondAllocLine) == -1, true);
let allocs2Lines = allocs2.map(x => x.line);
assertEq(allocs2Lines.indexOf(root.secondAllocLine) != -1, true);
assertEq(allocs2Lines.indexOf(root.firstAllocLine) == -1, true);

View File

@ -0,0 +1,20 @@
// Test logs that contain allocations from many debuggee compartments.
const dbg = new Debugger();
const root1 = newGlobal();
const root2 = newGlobal();
const root3 = newGlobal();
dbg.addDebuggee(root1);
dbg.addDebuggee(root2);
dbg.addDebuggee(root3);
dbg.memory.trackingAllocationSites = true;
root1.eval("this.alloc = {}");
root2.eval("this.alloc = {}");
root3.eval("this.alloc = {}");
const allocs = dbg.memory.drainAllocationsLog();
assertEq(allocs.length >= 3, true);

View File

@ -0,0 +1,21 @@
// Test logs that contain allocations from debuggee compartments added as we are
// logging.
const dbg = new Debugger();
dbg.memory.trackingAllocationSites = true;
const root1 = newGlobal();
dbg.addDebuggee(root1);
root1.eval("this.alloc = {}");
const root2 = newGlobal();
dbg.addDebuggee(root2);
root2.eval("this.alloc = {}");
const root3 = newGlobal();
dbg.addDebuggee(root3);
root3.eval("this.alloc = {}");
const allocs = dbg.memory.drainAllocationsLog();
assertEq(allocs.length >= 3, true);

View File

@ -0,0 +1,25 @@
// Test logs that shouldn't contain allocations from debuggee compartments
// removed as we are logging.
const dbg = new Debugger();
const root1 = newGlobal();
dbg.addDebuggee(root1);
const root2 = newGlobal();
dbg.addDebuggee(root2);
const root3 = newGlobal();
dbg.addDebuggee(root3);
dbg.memory.trackingAllocationSites = true;
dbg.removeDebuggee(root1);
root1.eval("this.alloc = {}");
dbg.removeDebuggee(root2);
root2.eval("this.alloc = {}");
dbg.removeDebuggee(root3);
root3.eval("this.alloc = {}");
const allocs = dbg.memory.drainAllocationsLog();
assertEq(allocs.length, 0);

View File

@ -0,0 +1,17 @@
// Test that disabling the debugger disables allocation tracking.
load(libdir + "asserts.js");
const dbg = new Debugger();
const root = newGlobal();
dbg.addDebuggee(root);
dbg.memory.trackingAllocationSites = true;
dbg.enabled = false;
root.eval("this.alloc = {}");
// We shouldn't accumulate allocations in our log while the debugger is
// disabled.
let allocs = dbg.memory.drainAllocationsLog();
assertEq(allocs.length, 0);

View File

@ -0,0 +1,18 @@
// Test that we don't crash while logging allocations and there is
// off-main-thread compilation. OMT compilation will allocate functions and
// regexps, but we just punt on measuring that accurately.
if (helperThreadCount() === 0) {
quit(0);
}
const root = newGlobal();
root.eval("this.dbg = new Debugger()");
root.dbg.addDebuggee(this);
root.dbg.memory.trackingAllocationSites = true;
offThreadCompileScript(
"function foo() {\n" +
" print('hello world');\n" +
"}"
);

View File

@ -240,7 +240,7 @@ MSG_DEF(JSMSG_SYMBOL_TO_STRING, 186, 0, JSEXN_TYPEERR, "can't convert symb
MSG_DEF(JSMSG_OBJECT_METADATA_CALLBACK_ALREADY_SET, 187, 0, JSEXN_ERR, "Cannot track object allocation, because other tools are already doing so")
MSG_DEF(JSMSG_INCOMPATIBLE_METHOD, 188, 3, JSEXN_TYPEERR, "{0} {1} called on incompatible {2}")
MSG_DEF(JSMSG_SYMBOL_TO_PRIMITIVE, 189, 0, JSEXN_TYPEERR, "can't convert symbol object to primitive")
MSG_DEF(JSMSG_UNUSED190, 190, 0, JSEXN_NONE, "")
MSG_DEF(JSMSG_NOT_TRACKING_ALLOCATIONS, 190, 1, JSEXN_ERR, "Cannot call {0} without setting trackingAllocationSites to true")
MSG_DEF(JSMSG_BAD_INDEX, 191, 0, JSEXN_RANGEERR, "invalid or out-of-range index")
MSG_DEF(JSMSG_SELFHOSTED_TOP_LEVEL_LET,192,0, JSEXN_SYNTAXERR, "self-hosted code cannot contain top-level 'let' declarations")
MSG_DEF(JSMSG_BAD_FOR_EACH_LOOP, 193, 0, JSEXN_SYNTAXERR, "invalid for each loop")

View File

@ -6,6 +6,8 @@
#include "vm/Debugger-inl.h"
#include "mozilla/DebugOnly.h"
#include "jscntxt.h"
#include "jscompartment.h"
#include "jshashutil.h"
@ -338,6 +340,7 @@ Breakpoint::nextInSite()
Debugger::Debugger(JSContext *cx, JSObject *dbg)
: object(dbg), uncaughtExceptionHook(nullptr), enabled(true), trackingAllocationSites(false),
allocationsLogLength(0), maxAllocationsLogLength(DEFAULT_MAX_ALLOCATIONS_LOG_LENGTH),
frames(cx->runtime()), scripts(cx), sources(cx), objects(cx), environments(cx)
{
assertSameCompartment(cx, dbg);
@ -350,6 +353,7 @@ Debugger::Debugger(JSContext *cx, JSObject *dbg)
Debugger::~Debugger()
{
JS_ASSERT_IF(debuggees.initialized(), debuggees.empty());
emptyAllocationsLog();
/*
* Since the inactive state for this link is a singleton cycle, it's always
@ -392,6 +396,19 @@ Debugger::fromChildJSObject(JSObject *obj)
return fromJSObject(dbgobj);
}
bool
Debugger::hasMemory() const
{
return object->getReservedSlot(JSSLOT_DEBUG_MEMORY_INSTANCE).isObject();
}
DebuggerMemory &
Debugger::memory() const
{
JS_ASSERT(hasMemory());
return object->getReservedSlot(JSSLOT_DEBUG_MEMORY_INSTANCE).toObject().as<DebuggerMemory>();
}
bool
Debugger::getScriptFrameWithIter(JSContext *cx, AbstractFramePtr frame,
const ScriptFrameIter *maybeIter, MutableHandleValue vp)
@ -1439,6 +1456,61 @@ Debugger::slowPathOnNewGlobalObject(JSContext *cx, Handle<GlobalObject *> global
JS_ASSERT(!cx->isExceptionPending());
}
/* static */ bool
Debugger::slowPathOnLogAllocationSite(JSContext *cx, HandleSavedFrame frame,
GlobalObject::DebuggerVector &dbgs)
{
JS_ASSERT(!dbgs.empty());
mozilla::DebugOnly<Debugger **> begin = dbgs.begin();
for (Debugger **dbgp = dbgs.begin(); dbgp < dbgs.end(); dbgp++) {
// The set of debuggers had better not change while we're iterating,
// such that the vector gets reallocated.
JS_ASSERT(dbgs.begin() == begin);
if ((*dbgp)->trackingAllocationSites &&
(*dbgp)->enabled &&
!(*dbgp)->appendAllocationSite(cx, frame))
{
return false;
}
}
return true;
}
bool
Debugger::appendAllocationSite(JSContext *cx, HandleSavedFrame frame)
{
AutoCompartment ac(cx, object);
RootedObject wrapped(cx, frame);
if (!cx->compartment()->wrap(cx, &wrapped))
return false;
AllocationSite *allocSite = cx->new_<AllocationSite>(wrapped);
if (!allocSite)
return false;
allocationsLog.insertBack(allocSite);
if (allocationsLogLength >= maxAllocationsLogLength) {
js_delete(allocationsLog.getFirst());
} else {
allocationsLogLength++;
}
return true;
}
void
Debugger::emptyAllocationsLog()
{
while (!allocationsLog.isEmpty())
js_delete(allocationsLog.getFirst());
allocationsLogLength = 0;
}
/*** Debugger JSObjects **************************************************************************/
@ -1634,6 +1706,12 @@ Debugger::trace(JSTracer *trc)
MarkObject(trc, &frameobj, "live Debugger.Frame");
}
/*
* Mark every allocation site in our allocation log.
*/
for (AllocationSite *s = allocationsLog.getFirst(); s; s = s->getNext())
MarkObject(trc, &s->frame, "allocation log SavedFrame");
/* Trace the weak map from JSScript instances to Debugger.Script objects. */
scripts.trace(trc);

View File

@ -14,14 +14,17 @@
#include "jscntxt.h"
#include "jscompartment.h"
#include "jsweakmap.h"
#include "jswrapper.h"
#include "gc/Barrier.h"
#include "js/HashTable.h"
#include "vm/GlobalObject.h"
#include "vm/SavedStacks.h"
namespace js {
class Breakpoint;
class DebuggerMemory;
/*
* A weakmap that supports the keys being in different compartments to the
@ -161,6 +164,7 @@ class Debugger : private mozilla::LinkedListElement<Debugger>
friend class DebuggerMemory;
friend class mozilla::LinkedListElement<Debugger>;
friend bool (::JS_DefineDebuggerObject)(JSContext *cx, JS::HandleObject obj);
friend bool SavedStacksMetadataCallback(JSContext *cx, JSObject **pmetadata);
public:
enum Hook {
@ -190,9 +194,25 @@ class Debugger : private mozilla::LinkedListElement<Debugger>
GlobalObjectSet debuggees; /* Debuggee globals. Cross-compartment weak references. */
js::HeapPtrObject uncaughtExceptionHook; /* Strong reference. */
bool enabled;
bool trackingAllocationSites;
JSCList breakpoints; /* Circular list of all js::Breakpoints in this debugger */
struct AllocationSite : public mozilla::LinkedListElement<AllocationSite>
{
AllocationSite(HandleObject frame) : frame(frame) {
JS_ASSERT(UncheckedUnwrap(frame)->is<SavedFrame>());
};
RelocatablePtrObject frame;
};
typedef mozilla::LinkedList<AllocationSite> AllocationSiteList;
bool trackingAllocationSites;
AllocationSiteList allocationsLog;
size_t allocationsLogLength;
size_t maxAllocationsLogLength;
static const size_t DEFAULT_MAX_ALLOCATIONS_LOG_LENGTH = 5000;
bool appendAllocationSite(JSContext *cx, HandleSavedFrame frame);
void emptyAllocationsLog();
/*
* If this Debugger is enabled, and has a onNewGlobalObject handler, then
* this link is inserted into the circular list headed by
@ -359,6 +379,8 @@ class Debugger : private mozilla::LinkedListElement<Debugger>
static void slowPathOnNewScript(JSContext *cx, HandleScript script,
GlobalObject *compileAndGoGlobal);
static void slowPathOnNewGlobalObject(JSContext *cx, Handle<GlobalObject *> global);
static bool slowPathOnLogAllocationSite(JSContext *cx, HandleSavedFrame frame,
GlobalObject::DebuggerVector &dbgs);
static JSTrapStatus dispatchHook(JSContext *cx, MutableHandleValue vp, Hook which);
JSTrapStatus fireDebuggerStatement(JSContext *cx, MutableHandleValue vp);
@ -408,6 +430,9 @@ class Debugger : private mozilla::LinkedListElement<Debugger>
static inline Debugger *fromJSObject(JSObject *obj);
static Debugger *fromChildJSObject(JSObject *obj);
bool hasMemory() const;
DebuggerMemory &memory() const;
/*********************************** Methods for interaction with the GC. */
/*
@ -441,6 +466,7 @@ class Debugger : private mozilla::LinkedListElement<Debugger>
static inline void onNewScript(JSContext *cx, HandleScript script,
GlobalObject *compileAndGoGlobal);
static inline void onNewGlobalObject(JSContext *cx, Handle<GlobalObject *> global);
static inline bool onLogAllocationSite(JSContext *cx, HandleSavedFrame frame);
static JSTrapStatus onTrap(JSContext *cx, MutableHandleValue vp);
static JSTrapStatus onSingleStep(JSContext *cx, MutableHandleValue vp);
static bool handleBaselineOsr(JSContext *cx, InterpreterFrame *from, jit::BaselineFrame *to);
@ -758,6 +784,15 @@ Debugger::onNewGlobalObject(JSContext *cx, Handle<GlobalObject *> global)
Debugger::slowPathOnNewGlobalObject(cx, global);
}
bool
Debugger::onLogAllocationSite(JSContext *cx, HandleSavedFrame frame)
{
GlobalObject::DebuggerVector *dbgs = frame->global().getDebuggers();
if (!dbgs || dbgs->empty())
return true;
return Debugger::slowPathOnLogAllocationSite(cx, frame, *dbgs);
}
extern bool
EvaluateInEnv(JSContext *cx, Handle<Env*> env, HandleValue thisv, AbstractFramePtr frame,
mozilla::Range<const jschar> chars, const char *filename, unsigned lineno,

View File

@ -7,6 +7,7 @@
#include "vm/DebuggerMemory.h"
#include "jscompartment.h"
#include "gc/Marking.h"
#include "vm/Debugger.h"
#include "vm/GlobalObject.h"
#include "vm/SavedStacks.h"
@ -31,6 +32,13 @@ DebuggerMemory::create(JSContext *cx, Debugger *dbg)
return &memory->as<DebuggerMemory>();
}
Debugger *
DebuggerMemory::getDebugger()
{
const Value &dbgVal = getReservedSlot(JSSLOT_DEBUGGER);
return Debugger::fromJSObject(&dbgVal.toObject());
}
/* static */ bool
DebuggerMemory::construct(JSContext *cx, unsigned argc, Value *vp)
{
@ -51,12 +59,6 @@ DebuggerMemory::construct(JSContext *cx, unsigned argc, Value *vp)
JS_EnumerateStub, // enumerate
JS_ResolveStub, // resolve
JS_ConvertStub, // convert
nullptr, // finalize
nullptr, // call
nullptr, // hasInstance
nullptr, // construct
nullptr // trace
};
/* static */ DebuggerMemory *
@ -108,12 +110,6 @@ DebuggerMemory::checkThis(JSContext *cx, CallArgs &args, const char *fnName)
if (!memory) \
return false
Debugger *
DebuggerMemory::getDebugger()
{
return Debugger::fromJSObject(&getReservedSlot(JSSLOT_DEBUGGER).toObject());
}
/* static */ bool
DebuggerMemory::setTrackingAllocationSites(JSContext *cx, unsigned argc, Value *vp)
{
@ -149,6 +145,9 @@ DebuggerMemory::setTrackingAllocationSites(JSContext *cx, unsigned argc, Value *
}
}
if (!enabling)
dbg->emptyAllocationsLog();
dbg->trackingAllocationSites = enabling;
args.rval().setUndefined();
return true;
@ -162,11 +161,81 @@ DebuggerMemory::getTrackingAllocationSites(JSContext *cx, unsigned argc, Value *
return true;
}
/* static */ bool
DebuggerMemory::drainAllocationsLog(JSContext *cx, unsigned argc, Value *vp)
{
THIS_DEBUGGER_MEMORY(cx, argc, vp, "drainAllocationsLog", args, memory);
Debugger* dbg = memory->getDebugger();
if (!dbg->trackingAllocationSites) {
JS_ReportErrorNumber(cx, js_GetErrorMessage, nullptr, JSMSG_NOT_TRACKING_ALLOCATIONS,
"drainAllocationsLog");
return false;
}
size_t length = dbg->allocationsLogLength;
RootedObject result(cx, NewDenseAllocatedArray(cx, length));
if (!result)
return false;
result->ensureDenseInitializedLength(cx, 0, length);
for (size_t i = 0; i < length; i++) {
Debugger::AllocationSite *allocSite = dbg->allocationsLog.popFirst();
result->setDenseElement(i, ObjectValue(*allocSite->frame));
js_delete(allocSite);
}
dbg->allocationsLogLength = 0;
args.rval().setObject(*result);
return true;
}
/* static */ bool
DebuggerMemory::getMaxAllocationsLogLength(JSContext *cx, unsigned argc, Value *vp)
{
THIS_DEBUGGER_MEMORY(cx, argc, vp, "(get maxAllocationsLogLength)", args, memory);
args.rval().setInt32(memory->getDebugger()->maxAllocationsLogLength);
return true;
}
/* static */ bool
DebuggerMemory::setMaxAllocationsLogLength(JSContext *cx, unsigned argc, Value *vp)
{
THIS_DEBUGGER_MEMORY(cx, argc, vp, "(set maxAllocationsLogLength)", args, memory);
if (!args.requireAtLeast(cx, "(set maxAllocationsLogLength)", 1))
return false;
int32_t max;
if (!ToInt32(cx, args[0], &max))
return false;
if (max < 1) {
JS_ReportErrorNumber(cx, js_GetErrorMessage, nullptr, JSMSG_UNEXPECTED_TYPE,
"(set maxAllocationsLogLength)'s parameter",
"not a positive integer");
return false;
}
Debugger *dbg = memory->getDebugger();
dbg->maxAllocationsLogLength = max;
while (dbg->allocationsLogLength > dbg->maxAllocationsLogLength) {
js_delete(dbg->allocationsLog.getFirst());
dbg->allocationsLogLength--;
}
args.rval().setUndefined();
return true;
}
/* static */ const JSPropertySpec DebuggerMemory::properties[] = {
JS_PSGS("trackingAllocationSites", getTrackingAllocationSites, setTrackingAllocationSites, 0),
JS_PSGS("maxAllocationsLogLength", getMaxAllocationsLogLength, setMaxAllocationsLogLength, 0),
JS_PS_END
};
/* static */ const JSFunctionSpec DebuggerMemory::methods[] = {
JS_FN("drainAllocationsLog", DebuggerMemory::drainAllocationsLog, 0, 0),
JS_FS_END
};

View File

@ -37,6 +37,9 @@ class DebuggerMemory : public JSObject {
static bool setTrackingAllocationSites(JSContext *cx, unsigned argc, Value *vp);
static bool getTrackingAllocationSites(JSContext *cx, unsigned argc, Value *vp);
static bool drainAllocationsLog(JSContext *cx, unsigned argc, Value *vp);
static bool setMaxAllocationsLogLength(JSContext*cx, unsigned argc, Value *vp);
static bool getMaxAllocationsLogLength(JSContext*cx, unsigned argc, Value *vp);
};
} /* namespace js */

View File

@ -14,6 +14,7 @@
#include "gc/Marking.h"
#include "js/Vector.h"
#include "vm/Debugger.h"
#include "vm/GlobalObject.h"
#include "vm/StringBuffer.h"
@ -734,7 +735,8 @@ SavedStacksMetadataCallback(JSContext *cx, JSObject **pmetadata)
if (!cx->compartment()->savedStacks().saveCurrentStack(cx, &frame))
return false;
*pmetadata = frame;
return true;
return Debugger::onLogAllocationSite(cx, frame);
}
#ifdef JS_CRASH_DIAGNOSTICS