diff --git a/js/src/doc/Debugger/Debugger.Memory.md b/js/src/doc/Debugger/Debugger.Memory.md
index 304bfaf4e27..98e24cb2de3 100644
--- a/js/src/doc/Debugger/Debugger.Memory.md
+++ b/js/src/doc/Debugger/Debugger.Memory.md
@@ -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:
+
+ {
+ "objects": { class: tally, ... },
+ "scripts": tally,
+ "strings": tally,
+ "other": { type name: tally, ... }
+ }
+
+
+ Each tally has the form:
+
{ "count": count }
- where count is the number of nodes found.
+ where count 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
+ class 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
--------------------------------------------------
diff --git a/js/src/jit-test/tests/debug/Memory-takeCensus-02.js b/js/src/jit-test/tests/debug/Memory-takeCensus-02.js
index cde3a662964..10babe34b92 100644
--- a/js/src/jit-test/tests/debug/Memory-takeCensus-02.js
+++ b/js/src/jit-test/tests/debug/Memory-takeCensus-02.js
@@ -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);
diff --git a/js/src/vm/DebuggerMemory.cpp b/js/src/vm/DebuggerMemory.cpp
index 89568a2e91a..839849ff8a4 100644
--- a/js/src/vm/DebuggerMemory.cpp
+++ b/js/src/vm/DebuggerMemory.cpp
@@ -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
+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())
+ return objects.count(census, node);
+ if (node.is() || node.is() || node.is())
+ return scripts.count(census, node);
+ if (node.is())
+ 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
+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 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())
+ return other.count(census, node);
+
+ const js::Class *key = node.as()->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
+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, 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
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, Tally, Tally, ByUbinodeType > DefaultAssorter;
-// A traversal that conducts a trivial census.
-typedef CensusHandler TallyingHandler;
-typedef BreadthFirst TallyingTraversal;
+// A traversal that conducts a census using DefaultAssorter.
+typedef CensusHandler DefaultCensusHandler;
+typedef BreadthFirst 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;