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;