Bug 1061909: Implement 'allocationStack' breakdown for Debugger.Memory.prototype.takeCensus. r=fitzgen

This commit is contained in:
Jim Blandy 2015-06-30 23:47:37 -07:00
parent 5ceab24634
commit 048f2b5f03
4 changed files with 256 additions and 1 deletions

View File

@ -0,0 +1,74 @@
// Debugger.Memory.prototype.takeCensus: by: allocationStack breakdown
var g = newGlobal();
var dbg = new Debugger(g);
g.evaluate(`
var log = [];
function f() { log.push(allocationMarker()); }
function g() { f(); }
function h() { f(); }
`,
{ fileName: "Rockford", lineNumber: 1000 });
// Create one allocationMarker with tracking turned off,
// so it will have no associated stack.
g.f();
dbg.memory.allocationSamplingProbability = 1;
dbg.memory.trackingAllocationSites = true;
for ([f, n] of [[g.f, 20], [g.g, 10], [g.h, 5]])
for (let i = 0; i < n; i++)
f(); // all allocations of allocationMarker occur with this line as the
// oldest stack frame.
let census = dbg.memory.takeCensus({ breakdown: { by: 'objectClass',
then: { by: 'allocationStack',
then: { by: 'count',
label: 'haz stack'
},
noStack: { by: 'count',
label: 'no haz stack'
}
}
}
});
let map = census.AllocationMarker;
assertEq(map instanceof Map, true);
// Gather the stacks we are expecting to appear as keys, and
// check that there are no unexpected keys.
let stacks = { };
map.forEach((v, k) => {
if (k === 'noStack') {
// No need to save this key.
} else if (k.functionDisplayName === 'f' &&
k.parent.functionDisplayName === null) {
stacks.f = k;
} else if (k.functionDisplayName === 'f' &&
k.parent.functionDisplayName === 'g' &&
k.parent.parent.functionDisplayName === null) {
stacks.fg = k;
} else if (k.functionDisplayName === 'f' &&
k.parent.functionDisplayName === 'h' &&
k.parent.parent.functionDisplayName === null) {
stacks.fh = k;
} else {
assertEq(true, false);
}
});
assertEq(map.get('noStack').label, 'no haz stack');
assertEq(map.get('noStack').count, 1);
assertEq(map.get(stacks.f).label, 'haz stack');
assertEq(map.get(stacks.f).count, 20);
assertEq(map.get(stacks.fg).label, 'haz stack');
assertEq(map.get(stacks.fg).count, 10);
assertEq(map.get(stacks.fh).label, 'haz stack');
assertEq(map.get(stacks.fh).count, 5);

View File

@ -4548,7 +4548,6 @@ EntryPoints(JSContext* cx, unsigned argc, Value* vp)
return false;
}
static const JSFunctionSpecWithHelp shell_functions[] = {
JS_FN_HELP("version", Version, 0, 0,
"version([number])",

View File

@ -151,6 +151,7 @@
macro(NFKC, NFKC, "NFKC") \
macro(NFKD, NFKD, "NFKD") \
macro(nonincrementalReason, nonincrementalReason, "nonincrementalReason") \
macro(noStack, noStack, "noStack") \
macro(noSuchMethod, noSuchMethod, "__noSuchMethod__") \
macro(NumberFormat, NumberFormat, "NumberFormat") \
macro(NumberFormatFormatGet, NumberFormatFormatGet, "Intl_NumberFormat_format_get") \

View File

@ -16,6 +16,7 @@
#include "jsalloc.h"
#include "jscompartment.h"
#include "builtin/MapObject.h"
#include "gc/Marking.h"
#include "js/Debug.h"
#include "js/TracingAPI.h"
@ -976,6 +977,175 @@ class ByUbinodeType : public CountType {
};
// A count type that categorizes nodes by the JS stack under which they were
// allocated.
class ByAllocationStack : public CountType {
typedef HashMap<SavedFrame*, CountBasePtr, DefaultHasher<SavedFrame*>,
SystemAllocPolicy> Table;
typedef Table::Entry Entry;
struct Count : public CountBase {
// NOTE: You may look up entries in this table by SavedFrame key only
// during traversal, NOT ONCE TRAVERSAL IS COMPLETE. Once traversal is
// complete, you may only iterate over it.
//
// In this hash table, keys are JSObjects, and we use JSObject identity
// (that is, address identity) as key identity. The normal way to
// support such a table is to make the trace function notice keys that
// have moved and re-key them in the table. However, our trace function
// does *not* rehash; the first GC may render the hash table
// unsearchable.
//
// This is as it should be:
//
// First, the heap traversal phase needs lookups by key to work. But no
// GC may ever occur during a traversal; this is enforced by the
// JS::ubi::BreadthFirst template. So the traceCount function doesn't
// need to do anything to help traversal; it never even runs then.
//
// Second, the report phase needs iteration over the table to work, but
// never looks up entries by key. GC may well occur during this phase:
// we allocate a Map object, and probably cross-compartment wrappers for
// SavedFrame instances as well. If a GC were to occur, it would call
// our traceCount function; if traceCount were to re-key, that would
// ruin the traversal in progress.
//
// So depending on the phase, we either don't need re-keying, or
// can't abide it.
Table table;
CountBasePtr noStack;
Count(CountType& type, CountBasePtr& noStack)
: CountBase(type),
noStack(Move(noStack))
{ }
bool init() { return table.init(); }
};
CountTypePtr entryType;
CountTypePtr noStackType;
public:
ByAllocationStack(Census& census, CountTypePtr& entryType, CountTypePtr& noStackType)
: CountType(census),
entryType(Move(entryType)),
noStackType(Move(noStackType))
{ }
CountBasePtr makeCount() override {
CountBasePtr noStackCount(noStackType->makeCount());
if (!noStackCount)
return nullptr;
UniquePtr<Count> count(census.new_<Count>(*this, noStackCount));
if (!count || !count->init())
return nullptr;
return CountBasePtr(count.release());
}
void traceCount(CountBase& countBase, JSTracer* trc) override {
Count& count= static_cast<Count&>(countBase);
for (Table::Range r = count.table.all(); !r.empty(); r.popFront()) {
// Trace our child Counts.
r.front().value()->trace(trc);
// Trace the SavedFrame that is this entry's key. Do not re-key if
// it has moved; see comments for ByAllocationStack::Count::table.
SavedFrame** keyPtr = const_cast<SavedFrame**>(&r.front().key());
TraceRoot(trc, keyPtr, "Debugger.Memory.prototype.census byAllocationStack count key");
}
count.noStack->trace(trc);
}
void destructCount(CountBase& countBase) override {
Count& count = static_cast<Count&>(countBase);
count.~Count();
}
bool count(CountBase& countBase, const Node& node) {
Count& count = static_cast<Count&>(countBase);
count.total_++;
SavedFrame* allocationStack = nullptr;
if (node.is<JSObject>()) {
JSObject* metadata = GetObjectMetadata(node.as<JSObject>());
if (metadata && metadata->is<SavedFrame>())
allocationStack = &metadata->as<SavedFrame>();
}
// If any other types had allocation site data, we could retrieve it
// here.
// If we do have an allocation stack for this node, include it in the
// count for that stack.
if (allocationStack) {
Table::AddPtr p = count.table.lookupForAdd(allocationStack);
if (!p) {
CountBasePtr stackCount(entryType->makeCount());
if (!stackCount || !count.table.add(p, allocationStack, Move(stackCount)))
return false;
}
return p->value()->count(node);
}
// Otherwise, count it in the "no stack" category.
return count.noStack->count(node);
}
bool report(CountBase& countBase, MutableHandleValue report) override {
Count& count = static_cast<Count&>(countBase);
JSContext* cx = census.cx;
#ifdef DEBUG
// Check that nothing rehashes our table while we hold pointers into it.
uint32_t generation = count.table.generation();
#endif
// Build a vector of pointers to entries; sort by total; and then use
// that to build the result object. This makes the ordering of entries
// more interesting, and a little less non-deterministic.
mozilla::Vector<Entry*> entries;
if (!entries.reserve(count.table.count()))
return false;
for (Table::Range r = count.table.all(); !r.empty(); r.popFront())
entries.infallibleAppend(&r.front());
qsort(entries.begin(), entries.length(), sizeof(*entries.begin()), compareEntries<Entry>);
// Now build the result by iterating over the sorted vector.
Rooted<MapObject*> map(cx, MapObject::create(cx));
if (!map)
return false;
for (Entry** entryPtr = entries.begin(); entryPtr < entries.end(); entryPtr++) {
Entry& entry = **entryPtr;
MOZ_ASSERT(entry.key());
RootedValue stack(cx, ObjectValue(*entry.key()));
if (!cx->compartment()->wrap(cx, &stack))
return false;
CountBasePtr& stackCount = entry.value();
RootedValue stackReport(cx);
if (!stackCount->report(&stackReport))
return false;
if (!MapObject::set(cx, map, stack, stackReport))
return false;
}
RootedValue noStackReport(cx);
if (!count.noStack->report(&noStackReport))
return false;
RootedValue noStack(cx, StringValue(cx->names().noStack));
if (!MapObject::set(cx, map, noStack, noStackReport))
return false;
MOZ_ASSERT(generation == count.table.generation());
report.setObject(*map);
return true;
}
};
// A BreadthFirst handler type that conducts a census, using a CountBase to
// categorize and count each node.
class CensusHandler {
@ -1142,6 +1312,17 @@ ParseBreakdown(Census& census, HandleValue breakdownValue)
return CountTypePtr(census.new_<ByUbinodeType>(census, thenType));
}
if (StringEqualsAscii(by, "allocationStack")) {
CountTypePtr thenType(ParseChildBreakdown(census, breakdown, cx->names().then));
if (!thenType)
return nullptr;
CountTypePtr noStackType(ParseChildBreakdown(census, breakdown, cx->names().noStack));
if (!noStackType)
return nullptr;
return CountTypePtr(census.new_<ByAllocationStack>(census, thenType, noStackType));
}
// We didn't recognize the breakdown type; complain.
RootedString bySource(cx, ValueToSource(cx, byValue));
if (!bySource)