Bug 1012456: Add non-trivial breakdowns to Debugger.Memory.prototype.takeCensus. r=terrence

This commit is contained in:
Jim Blandy 2014-08-11 12:46:39 -07:00
parent 46ff4b8265
commit c88922f0bd
3 changed files with 303 additions and 18 deletions

View File

@ -65,11 +65,35 @@ per-category counts, whose size depends on the number of categories.
: Carry out a census of the debuggee compartments' contents. Return an
object of the form:
<pre class='language-js'><code>
{
"objects": { <i>class</i>: <i>tally</i>, ... },
"scripts": <i>tally</i>,
"strings": <i>tally</i>,
"other": { <i>type name</i>: <i>tally</i>, ... }
}
</code></pre>
Each <i>tally</i> has the form:
<pre class='language-js'><code>
{ "count": <i>count</i> }
</code></pre>
where <i>count</i> is the number of nodes found.
where <i>count</i> is the number of items in the category.
The `"objects"` property's value contains the tallies of JavaScript objects,
broken down by their ECMAScript `[[Class]]` internal property values. Each
<i>class</i> is a string.
The `"scripts"` property's value tallies the in-memory representation of
JavaScript code.
The `"strings"` property's value tallies the debuggee's strings.
The `"other"` property's value contains the tallies of other items used
internally by SpiderMonkey, broken down by their C++ type name.
Memory Use Analysis Exposes Implementation Details
--------------------------------------------------

View File

@ -8,8 +8,8 @@ Census.walkCensus(census0, "census0", Census.assertAllZeros);
var g1 = newGlobal();
g1.eval('var a = [];');
g1.eval('function add() { a.push({}); }');
g1.eval('function remove() { a.pop({}); }');
g1.eval('function add(f) { a.push({}); a.push(f ? (() => undefined) : null); }');
g1.eval('function remove() { a.pop(); a.pop(); }');
g1.add();
g1.remove();
@ -18,26 +18,37 @@ dbg.addDebuggee(g1);
var census1 = dbg.memory.takeCensus();
Census.walkCensus(census1, "census1", Census.assertAllNotLessThan(census0));
function pointCheck(label, lhs, rhs, objComp, funComp) {
print(label);
assertEq(objComp(lhs.objects.Object.count, rhs.objects.Object.count), true);
assertEq(funComp(lhs.objects.Function.count, rhs.objects.Function.count), true);
}
function eq(lhs, rhs) { return lhs === rhs; }
function lt(lhs, rhs) { return lhs < rhs; }
function gt(lhs, rhs) { return lhs > rhs; }
// As we increase the number of reachable objects, the census should
// reflect that.
g1.add();
g1.add(false);
var census2 = dbg.memory.takeCensus();
assertEq(census2.count > census1.count, true);
pointCheck("census2", census2, census1, gt, eq);
g1.add();
g1.add(true);
var census3 = dbg.memory.takeCensus();
assertEq(census3.count > census2.count, true);
pointCheck("census3", census3, census2, gt, gt);
g1.add();
g1.add(false);
var census4 = dbg.memory.takeCensus();
assertEq(census4.count > census3.count, true);
pointCheck("census4", census4, census3, gt, eq);
// As we decrease the number of reachable objects, the census counts should go
// down.
// down. Note that since the census does its own reachability analysis, we don't
// need to GC here to see the counts drop.
g1.remove();
var census5 = dbg.memory.takeCensus();
assertEq(census5.count < census4.count, true);
pointCheck("census5", census5, census4, lt, eq);
g1.remove();
var census6 = dbg.memory.takeCensus();
assertEq(census6.count < census5.count, true);
pointCheck("census6", census6, census5, lt, lt);

View File

@ -316,7 +316,252 @@ class Tally {
}
};
// A ubi::BreadthFirst handler type that conducts a census, using Assorter
// An assorter that breaks nodes down by their JavaScript type --- 'objects',
// 'strings', 'scripts', and 'other' --- and then passes the nodes to
// sub-assorters. The template arguments must themselves be assorter types.
//
// Implementation details of scripts like jitted code are counted under
// 'scripts'.
template<typename EachObject = Tally,
typename EachScript = Tally,
typename EachString = Tally,
typename EachOther = Tally>
class ByJSType {
EachObject objects;
EachScript scripts;
EachString strings;
EachOther other;
public:
ByJSType(Census &census)
: objects(census),
scripts(census),
strings(census),
other(census)
{ }
ByJSType(ByJSType &&rhs)
: objects(Move(rhs.objects)),
scripts(move(rhs.scripts)),
strings(move(rhs.strings)),
other(move(rhs.other))
{ }
ByJSType &operator=(ByJSType &&rhs) {
MOZ_ASSERT(&rhs != this);
this->~ByJSType();
new (this) ByJSType(Move(rhs));
return *this;
}
bool init(Census &census) {
return objects.init(census) &&
scripts.init(census) &&
strings.init(census) &&
other.init(census);
}
bool count(Census &census, const Node &node) {
if (node.is<JSObject>())
return objects.count(census, node);
if (node.is<JSScript>() || node.is<LazyScript>() || node.is<jit::JitCode>())
return scripts.count(census, node);
if (node.is<JSString>())
return strings.count(census, node);
return other.count(census, node);
}
bool report(Census &census, MutableHandleValue report) {
JSContext *cx = census.cx;
RootedObject obj(cx, NewBuiltinClassInstance(cx, &JSObject::class_));
if (!obj)
return false;
RootedValue objectsReport(cx);
if (!objects.report(census, &objectsReport) ||
!JSObject::defineProperty(cx, obj, cx->names().objects, objectsReport))
return false;
RootedValue scriptsReport(cx);
if (!scripts.report(census, &scriptsReport) ||
!JSObject::defineProperty(cx, obj, cx->names().scripts, scriptsReport))
return false;
RootedValue stringsReport(cx);
if (!strings.report(census, &stringsReport) ||
!JSObject::defineProperty(cx, obj, cx->names().strings, stringsReport))
return false;
RootedValue otherReport(cx);
if (!other.report(census, &otherReport) ||
!JSObject::defineProperty(cx, obj, cx->names().other, otherReport))
return false;
report.setObject(*obj);
return true;
}
};
// An assorter that categorizes nodes that are JSObjects by their class, and
// places all other nodes in an 'other' category. The template arguments must be
// assorter types; each JSObject class gets an EachClass assorter, and the
// 'other' category gets an EachOther assorter.
template<typename EachClass = Tally,
typename EachOther = Tally>
class ByObjectClass {
// A hash policy that compares js::Classes by name.
struct HashPolicy {
typedef const js::Class *Lookup;
static js::HashNumber hash(Lookup l) { return mozilla::HashString(l->name); }
static bool match(const js::Class *key, Lookup lookup) {
return strcmp(key->name, lookup->name) == 0;
}
};
// A table mapping classes to their counts. Note that this table treats
// js::Class instances with the same name as equal keys. If you have several
// js::Classes with equal names (and we do; as of this writing there were
// six named "Object"), you will get several different Classes being counted
// in the same table entry.
typedef HashMap<const js::Class *, EachClass, HashPolicy, SystemAllocPolicy> Table;
Table table;
EachOther other;
public:
ByObjectClass(Census &census) : other(census) { }
ByObjectClass(ByObjectClass &&rhs) : table(Move(rhs.table)), other(Move(rhs.other)) { }
ByObjectClass &operator=(ByObjectClass &&rhs) {
MOZ_ASSERT(&rhs != this);
this->~ByObjectClass();
new (this) ByObjectClass(Move(rhs));
return *this;
}
bool init(Census &census) { return table.init() && other.init(census); }
bool count(Census &census, const Node &node) {
if (!node.is<JSObject>())
return other.count(census, node);
const js::Class *key = node.as<JSObject>()->getClass();
typename Table::AddPtr p = table.lookupForAdd(key);
if (!p) {
if (!table.add(p, key, EachClass(census)))
return false;
if (!p->value().init(census))
return false;
}
return p->value().count(census, node);
}
bool report(Census &census, MutableHandleValue report) {
JSContext *cx = census.cx;
RootedObject obj(cx, NewBuiltinClassInstance(cx, &JSObject::class_));
if (!obj)
return false;
for (typename Table::Range r = table.all(); !r.empty(); r.popFront()) {
EachClass &entry = r.front().value();
RootedValue entryReport(cx);
if (!entry.report(census, &entryReport))
return false;
const char *name = r.front().key()->name;
MOZ_ASSERT(name);
JSAtom *atom = Atomize(census.cx, name, strlen(name));
if (!atom)
return false;
RootedId entryId(cx, AtomToId(atom));
#ifdef DEBUG
// We have multiple js::Classes out there with the same name (for
// example, JSObject::class_, Debugger.Object, and CollatorClass are
// all "Object"), so let's make sure our hash table treats them all
// as equivalent.
bool has;
if (!JSObject::hasProperty(cx, obj, entryId, &has))
return false;
if (has) {
fprintf(stderr, "already has %s\n", name);
MOZ_ASSERT(!has);
}
#endif
if (!JSObject::defineGeneric(cx, obj, entryId, entryReport))
return false;
}
report.setObject(*obj);
return true;
}
};
// An assorter that categorizes nodes by their ubi::Node::typeName.
template<typename EachType = Tally>
class ByUbinodeType {
// Note that, because ubi::Node::typeName promises to return a specific
// pointer, not just any string whose contents are correct, we can use their
// addresses as hash table keys.
typedef HashMap<const jschar *, EachType, DefaultHasher<const jschar *>, SystemAllocPolicy> Table;
Table table;
public:
ByUbinodeType(Census &census) { }
ByUbinodeType(ByUbinodeType &&rhs) : table(Move(rhs.table)) { }
ByUbinodeType &operator=(ByUbinodeType &&rhs) {
MOZ_ASSERT(&rhs != this);
this->~ByUbinodeType();
new (this) ByUbinodeType(Move(rhs));
return *this;
}
bool init(Census &census) { return table.init(); }
bool count(Census &census, const Node &node) {
const jschar *key = node.typeName();
typename Table::AddPtr p = table.lookupForAdd(key);
if (!p) {
if (!table.add(p, key, EachType(census)))
return false;
if (!p->value().init(census))
return false;
}
return p->value().count(census, node);
}
bool report(Census &census, MutableHandleValue report) {
JSContext *cx = census.cx;
RootedObject obj(cx, NewBuiltinClassInstance(cx, &JSObject::class_));
if (!obj)
return false;
for (typename Table::Range r = table.all(); !r.empty(); r.popFront()) {
EachType &entry = r.front().value();
RootedValue entryReport(cx);
if (!entry.report(census, &entryReport))
return false;
const jschar *name = r.front().key();
MOZ_ASSERT(name);
JSAtom *atom = AtomizeChars(cx, name, js_strlen(name));
if (!atom)
return false;
RootedId entryId(cx, AtomToId(atom));
if (!JSObject::defineGeneric(cx, obj, entryId, entryReport))
return false;
}
report.setObject(*obj);
return true;
}
};
// A BreadthFirst handler type that conducts a census, using Assorter
// to categorize and count each node.
template<typename Assorter>
class CensusHandler {
@ -366,10 +611,15 @@ class CensusHandler {
}
};
// The assorter that Debugger.Memory.prototype.takeCensus uses by default.
// (Eventually, we hope to add parameters that let you specify dynamically how
// the census should assort the nodes it finds.) Categorize nodes by JS type,
// and then objects by object class.
typedef ByJSType<ByObjectClass<Tally>, Tally, Tally, ByUbinodeType<Tally> > DefaultAssorter;
// A traversal that conducts a trivial census.
typedef CensusHandler<Tally> TallyingHandler;
typedef BreadthFirst<TallyingHandler> TallyingTraversal;
// A traversal that conducts a census using DefaultAssorter.
typedef CensusHandler<DefaultAssorter> DefaultCensusHandler;
typedef BreadthFirst<DefaultCensusHandler> DefaultCensusTraversal;
} // namespace dbg
} // namespace js
@ -383,14 +633,14 @@ DebuggerMemory::takeCensus(JSContext *cx, unsigned argc, Value *vp)
dbg::Census census(cx);
if (!census.init())
return false;
dbg::TallyingHandler handler(census);
dbg::DefaultCensusHandler handler(census);
if (!handler.init(census))
return false;
{
JS::AutoCheckCannotGC noGC;
dbg::TallyingTraversal traversal(cx, handler, noGC);
dbg::DefaultCensusTraversal traversal(cx, handler, noGC);
if (!traversal.init())
return false;