diff --git a/js/src/jit-test/tests/debug/Memory-takeCensus-09.js b/js/src/jit-test/tests/debug/Memory-takeCensus-09.js new file mode 100644 index 00000000000..09b28da3425 --- /dev/null +++ b/js/src/jit-test/tests/debug/Memory-takeCensus-09.js @@ -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); diff --git a/js/src/shell/js.cpp b/js/src/shell/js.cpp index 6ca1d40db59..3fadb1731d8 100644 --- a/js/src/shell/js.cpp +++ b/js/src/shell/js.cpp @@ -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])", diff --git a/js/src/vm/CommonPropertyNames.h b/js/src/vm/CommonPropertyNames.h index 8cbbf0e1f6b..3fc3fb5bf94 100644 --- a/js/src/vm/CommonPropertyNames.h +++ b/js/src/vm/CommonPropertyNames.h @@ -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") \ diff --git a/js/src/vm/DebuggerMemory.cpp b/js/src/vm/DebuggerMemory.cpp index c4246a2599c..7c447b53493 100644 --- a/js/src/vm/DebuggerMemory.cpp +++ b/js/src/vm/DebuggerMemory.cpp @@ -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, + 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(census.new_(*this, noStackCount)); + if (!count || !count->init()) + return nullptr; + return CountBasePtr(count.release()); + } + + void traceCount(CountBase& countBase, JSTracer* trc) override { + Count& count= static_cast(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(&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(countBase); + count.~Count(); + } + + bool count(CountBase& countBase, const Node& node) { + Count& count = static_cast(countBase); + count.total_++; + + SavedFrame* allocationStack = nullptr; + if (node.is()) { + JSObject* metadata = GetObjectMetadata(node.as()); + if (metadata && metadata->is()) + allocationStack = &metadata->as(); + } + // 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(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 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); + + // Now build the result by iterating over the sorted vector. + Rooted 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_(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_(census, thenType, noStackType)); + } + // We didn't recognize the breakdown type; complain. RootedString bySource(cx, ValueToSource(cx, byValue)); if (!bySource)