summaryrefslogtreecommitdiffstats
path: root/js/src/vm/UbiNodeCensus.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'js/src/vm/UbiNodeCensus.cpp')
-rw-r--r--js/src/vm/UbiNodeCensus.cpp1167
1 files changed, 1167 insertions, 0 deletions
diff --git a/js/src/vm/UbiNodeCensus.cpp b/js/src/vm/UbiNodeCensus.cpp
new file mode 100644
index 000000000..e2c56c666
--- /dev/null
+++ b/js/src/vm/UbiNodeCensus.cpp
@@ -0,0 +1,1167 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 4 -*-
+ * vim: set ts=8 sts=4 et sw=4 tw=99:
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "js/UbiNodeCensus.h"
+
+#include "jscntxt.h"
+#include "jscompartment.h"
+#include "jsobjinlines.h"
+
+#include "vm/NativeObject-inl.h"
+
+using namespace js;
+
+namespace JS {
+namespace ubi {
+
+JS_PUBLIC_API(void)
+CountDeleter::operator()(CountBase* ptr)
+{
+ if (!ptr)
+ return;
+
+ // Downcast to our true type and destruct, as guided by our CountType
+ // pointer.
+ ptr->destruct();
+ js_free(ptr);
+}
+
+JS_PUBLIC_API(bool)
+Census::init() {
+ AutoLockForExclusiveAccess lock(cx);
+ atomsZone = cx->runtime()->atomsCompartment(lock)->zone();
+ return targetZones.init();
+}
+
+
+/*** Count Types ***********************************************************************************/
+
+// The simplest type: just count everything.
+class SimpleCount : public CountType {
+
+ struct Count : CountBase {
+ size_t totalBytes_;
+
+ explicit Count(SimpleCount& count)
+ : CountBase(count),
+ totalBytes_(0)
+ { }
+ };
+
+ UniqueTwoByteChars label;
+ bool reportCount : 1;
+ bool reportBytes : 1;
+
+ public:
+ explicit SimpleCount(UniqueTwoByteChars& label,
+ bool reportCount=true,
+ bool reportBytes=true)
+ : CountType(),
+ label(Move(label)),
+ reportCount(reportCount),
+ reportBytes(reportBytes)
+ { }
+
+ explicit SimpleCount()
+ : CountType(),
+ label(nullptr),
+ reportCount(true),
+ reportBytes(true)
+ { }
+
+ void destructCount(CountBase& countBase) override {
+ Count& count = static_cast<Count&>(countBase);
+ count.~Count();
+ }
+
+ CountBasePtr makeCount() override { return CountBasePtr(js_new<Count>(*this)); }
+ void traceCount(CountBase& countBase, JSTracer* trc) override { }
+ bool count(CountBase& countBase, mozilla::MallocSizeOf mallocSizeOf, const Node& node) override;
+ bool report(JSContext* cx, CountBase& countBase, MutableHandleValue report) override;
+};
+
+bool
+SimpleCount::count(CountBase& countBase, mozilla::MallocSizeOf mallocSizeOf, const Node& node)
+{
+ Count& count = static_cast<Count&>(countBase);
+ if (reportBytes)
+ count.totalBytes_ += node.size(mallocSizeOf);
+ return true;
+}
+
+bool
+SimpleCount::report(JSContext* cx, CountBase& countBase, MutableHandleValue report)
+{
+ Count& count = static_cast<Count&>(countBase);
+
+ RootedPlainObject obj(cx, NewBuiltinClassInstance<PlainObject>(cx));
+ if (!obj)
+ return false;
+
+ RootedValue countValue(cx, NumberValue(count.total_));
+ if (reportCount && !DefineProperty(cx, obj, cx->names().count, countValue))
+ return false;
+
+ RootedValue bytesValue(cx, NumberValue(count.totalBytes_));
+ if (reportBytes && !DefineProperty(cx, obj, cx->names().bytes, bytesValue))
+ return false;
+
+ if (label) {
+ JSString* labelString = JS_NewUCStringCopyZ(cx, label.get());
+ if (!labelString)
+ return false;
+ RootedValue labelValue(cx, StringValue(labelString));
+ if (!DefineProperty(cx, obj, cx->names().label, labelValue))
+ return false;
+ }
+
+ report.setObject(*obj);
+ return true;
+}
+
+
+// A count type that collects all matching nodes in a bucket.
+class BucketCount : public CountType {
+
+ struct Count : CountBase {
+ JS::ubi::Vector<JS::ubi::Node::Id> ids_;
+
+ explicit Count(BucketCount& count)
+ : CountBase(count),
+ ids_()
+ { }
+ };
+
+ public:
+ explicit BucketCount()
+ : CountType()
+ { }
+
+ void destructCount(CountBase& countBase) override {
+ Count& count = static_cast<Count&>(countBase);
+ count.~Count();
+ }
+
+ CountBasePtr makeCount() override { return CountBasePtr(js_new<Count>(*this)); }
+ void traceCount(CountBase& countBase, JSTracer* trc) final { }
+ bool count(CountBase& countBase, mozilla::MallocSizeOf mallocSizeOf, const Node& node) override;
+ bool report(JSContext* cx, CountBase& countBase, MutableHandleValue report) override;
+};
+
+bool
+BucketCount::count(CountBase& countBase, mozilla::MallocSizeOf mallocSizeOf, const Node& node)
+{
+ Count& count = static_cast<Count&>(countBase);
+ return count.ids_.append(node.identifier());
+}
+
+bool
+BucketCount::report(JSContext* cx, CountBase& countBase, MutableHandleValue report)
+{
+ Count& count = static_cast<Count&>(countBase);
+
+ size_t length = count.ids_.length();
+ RootedArrayObject arr(cx, NewDenseFullyAllocatedArray(cx, length));
+ if (!arr)
+ return false;
+ arr->ensureDenseInitializedLength(cx, 0, length);
+
+ for (size_t i = 0; i < length; i++)
+ arr->setDenseElement(i, NumberValue(count.ids_[i]));
+
+ report.setObject(*arr);
+ return true;
+}
+
+
+// A type that categorizes nodes by their JavaScript type -- 'objects',
+// 'strings', 'scripts', and 'other' -- and then passes the nodes to child
+// types.
+//
+// Implementation details of scripts like jitted code are counted under
+// 'scripts'.
+class ByCoarseType : public CountType {
+ CountTypePtr objects;
+ CountTypePtr scripts;
+ CountTypePtr strings;
+ CountTypePtr other;
+
+ struct Count : CountBase {
+ Count(CountType& type,
+ CountBasePtr& objects,
+ CountBasePtr& scripts,
+ CountBasePtr& strings,
+ CountBasePtr& other)
+ : CountBase(type),
+ objects(Move(objects)),
+ scripts(Move(scripts)),
+ strings(Move(strings)),
+ other(Move(other))
+ { }
+
+ CountBasePtr objects;
+ CountBasePtr scripts;
+ CountBasePtr strings;
+ CountBasePtr other;
+ };
+
+ public:
+ ByCoarseType(CountTypePtr& objects,
+ CountTypePtr& scripts,
+ CountTypePtr& strings,
+ CountTypePtr& other)
+ : CountType(),
+ objects(Move(objects)),
+ scripts(Move(scripts)),
+ strings(Move(strings)),
+ other(Move(other))
+ { }
+
+ void destructCount(CountBase& countBase) override {
+ Count& count = static_cast<Count&>(countBase);
+ count.~Count();
+ }
+
+ CountBasePtr makeCount() override;
+ void traceCount(CountBase& countBase, JSTracer* trc) override;
+ bool count(CountBase& countBase, mozilla::MallocSizeOf mallocSizeOf, const Node& node) override;
+ bool report(JSContext* cx, CountBase& countBase, MutableHandleValue report) override;
+};
+
+CountBasePtr
+ByCoarseType::makeCount()
+{
+ CountBasePtr objectsCount(objects->makeCount());
+ CountBasePtr scriptsCount(scripts->makeCount());
+ CountBasePtr stringsCount(strings->makeCount());
+ CountBasePtr otherCount(other->makeCount());
+
+ if (!objectsCount || !scriptsCount || !stringsCount || !otherCount)
+ return CountBasePtr(nullptr);
+
+ return CountBasePtr(js_new<Count>(*this,
+ objectsCount,
+ scriptsCount,
+ stringsCount,
+ otherCount));
+}
+
+void
+ByCoarseType::traceCount(CountBase& countBase, JSTracer* trc)
+{
+ Count& count = static_cast<Count&>(countBase);
+ count.objects->trace(trc);
+ count.scripts->trace(trc);
+ count.strings->trace(trc);
+ count.other->trace(trc);
+}
+
+bool
+ByCoarseType::count(CountBase& countBase, mozilla::MallocSizeOf mallocSizeOf, const Node& node)
+{
+ Count& count = static_cast<Count&>(countBase);
+
+ switch (node.coarseType()) {
+ case JS::ubi::CoarseType::Object:
+ return count.objects->count(mallocSizeOf, node);
+ case JS::ubi::CoarseType::Script:
+ return count.scripts->count(mallocSizeOf, node);
+ case JS::ubi::CoarseType::String:
+ return count.strings->count(mallocSizeOf, node);
+ case JS::ubi::CoarseType::Other:
+ return count.other->count(mallocSizeOf, node);
+ default:
+ MOZ_CRASH("bad JS::ubi::CoarseType in JS::ubi::ByCoarseType::count");
+ return false;
+ }
+}
+
+bool
+ByCoarseType::report(JSContext* cx, CountBase& countBase, MutableHandleValue report)
+{
+ Count& count = static_cast<Count&>(countBase);
+
+ RootedPlainObject obj(cx, NewBuiltinClassInstance<PlainObject>(cx));
+ if (!obj)
+ return false;
+
+ RootedValue objectsReport(cx);
+ if (!count.objects->report(cx, &objectsReport) ||
+ !DefineProperty(cx, obj, cx->names().objects, objectsReport))
+ return false;
+
+ RootedValue scriptsReport(cx);
+ if (!count.scripts->report(cx, &scriptsReport) ||
+ !DefineProperty(cx, obj, cx->names().scripts, scriptsReport))
+ return false;
+
+ RootedValue stringsReport(cx);
+ if (!count.strings->report(cx, &stringsReport) ||
+ !DefineProperty(cx, obj, cx->names().strings, stringsReport))
+ return false;
+
+ RootedValue otherReport(cx);
+ if (!count.other->report(cx, &otherReport) ||
+ !DefineProperty(cx, obj, cx->names().other, otherReport))
+ return false;
+
+ report.setObject(*obj);
+ return true;
+}
+
+
+// Comparison function for sorting hash table entries by the smallest node ID
+// they counted. Node IDs are stable and unique, which ensures ordering of
+// results never depends on hash table placement or sort algorithm vagaries. The
+// arguments are doubly indirect: they're pointers to elements in an array of
+// pointers to table entries.
+template<typename Entry>
+static int compareEntries(const void* lhsVoid, const void* rhsVoid) {
+ auto lhs = (*static_cast<const Entry* const*>(lhsVoid))->value()->smallestNodeIdCounted_;
+ auto rhs = (*static_cast<const Entry* const*>(rhsVoid))->value()->smallestNodeIdCounted_;
+
+ // We don't want to just subtract the values, as they're unsigned.
+ if (lhs < rhs)
+ return 1;
+ if (lhs > rhs)
+ return -1;
+ return 0;
+}
+
+// A hash map mapping from C strings to counts.
+using CStringCountMap = HashMap<const char*, CountBasePtr, CStringHasher, SystemAllocPolicy>;
+
+// Convert a HashMap into an object with each key one of the entries from the
+// map and each value the associated count's report. For use during census
+// reporting.
+//
+// `Map` must be a `HashMap` from some key type to a `CountBasePtr`.
+//
+// `GetName` must be a callable type which takes `const Map::Key&` and returns
+// `const char*`.
+template <class Map, class GetName>
+static PlainObject*
+countMapToObject(JSContext* cx, Map& map, GetName getName) {
+ // 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.
+
+ JS::ubi::Vector<typename Map::Entry*> entries;
+ if (!entries.reserve(map.count())) {
+ ReportOutOfMemory(cx);
+ return nullptr;
+ }
+
+ for (auto r = map.all(); !r.empty(); r.popFront())
+ entries.infallibleAppend(&r.front());
+
+ qsort(entries.begin(), entries.length(), sizeof(*entries.begin()),
+ compareEntries<typename Map::Entry>);
+
+ RootedPlainObject obj(cx, NewBuiltinClassInstance<PlainObject>(cx));
+ if (!obj)
+ return nullptr;
+
+ for (auto& entry : entries) {
+ CountBasePtr& thenCount = entry->value();
+ RootedValue thenReport(cx);
+ if (!thenCount->report(cx, &thenReport))
+ return nullptr;
+
+ const char* name = getName(entry->key());
+ MOZ_ASSERT(name);
+ JSAtom* atom = Atomize(cx, name, strlen(name));
+ if (!atom)
+ return nullptr;
+
+ RootedId entryId(cx, AtomToId(atom));
+ if (!DefineProperty(cx, obj, entryId, thenReport))
+ return nullptr;
+ }
+
+ return obj;
+}
+
+
+// A type that categorizes nodes that are JSObjects by their class name,
+// and places all other nodes in an 'other' category.
+class ByObjectClass : public CountType {
+ // A table mapping class names to their counts. Note that we treat 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 js::Classes being
+ // counted in the same table entry.
+ using Table = CStringCountMap;
+ using Entry = Table::Entry;
+
+ struct Count : public CountBase {
+ Table table;
+ CountBasePtr other;
+
+ Count(CountType& type, CountBasePtr& other)
+ : CountBase(type),
+ other(Move(other))
+ { }
+
+ bool init() { return table.init(); }
+ };
+
+ CountTypePtr classesType;
+ CountTypePtr otherType;
+
+ public:
+ ByObjectClass(CountTypePtr& classesType, CountTypePtr& otherType)
+ : CountType(),
+ classesType(Move(classesType)),
+ otherType(Move(otherType))
+ { }
+
+ void destructCount(CountBase& countBase) override {
+ Count& count = static_cast<Count&>(countBase);
+ count.~Count();
+ }
+
+ CountBasePtr makeCount() override;
+ void traceCount(CountBase& countBase, JSTracer* trc) override;
+ bool count(CountBase& countBase, mozilla::MallocSizeOf mallocSizeOf, const Node& node) override;
+ bool report(JSContext* cx, CountBase& countBase, MutableHandleValue report) override;
+};
+
+CountBasePtr
+ByObjectClass::makeCount()
+{
+ CountBasePtr otherCount(otherType->makeCount());
+ if (!otherCount)
+ return nullptr;
+
+ UniquePtr<Count> count(js_new<Count>(*this, otherCount));
+ if (!count || !count->init())
+ return nullptr;
+
+ return CountBasePtr(count.release());
+}
+
+void
+ByObjectClass::traceCount(CountBase& countBase, JSTracer* trc)
+{
+ Count& count = static_cast<Count&>(countBase);
+ for (Table::Range r = count.table.all(); !r.empty(); r.popFront())
+ r.front().value()->trace(trc);
+ count.other->trace(trc);
+}
+
+bool
+ByObjectClass::count(CountBase& countBase, mozilla::MallocSizeOf mallocSizeOf, const Node& node)
+{
+ Count& count = static_cast<Count&>(countBase);
+
+ const char* className = node.jsObjectClassName();
+ if (!className)
+ return count.other->count(mallocSizeOf, node);
+
+ Table::AddPtr p = count.table.lookupForAdd(className);
+ if (!p) {
+ CountBasePtr classCount(classesType->makeCount());
+ if (!classCount || !count.table.add(p, className, Move(classCount)))
+ return false;
+ }
+ return p->value()->count(mallocSizeOf, node);
+}
+
+bool
+ByObjectClass::report(JSContext* cx, CountBase& countBase, MutableHandleValue report)
+{
+ Count& count = static_cast<Count&>(countBase);
+
+ RootedPlainObject obj(cx, countMapToObject(cx, count.table, [](const char* key) {
+ return key;
+ }));
+ if (!obj)
+ return false;
+
+ RootedValue otherReport(cx);
+ if (!count.other->report(cx, &otherReport) ||
+ !DefineProperty(cx, obj, cx->names().other, otherReport))
+ return false;
+
+ report.setObject(*obj);
+ return true;
+}
+
+
+// A count type that categorizes nodes by their ubi::Node::typeName.
+class ByUbinodeType : public CountType {
+ // 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.
+ using Table = HashMap<const char16_t*, CountBasePtr, DefaultHasher<const char16_t*>,
+ SystemAllocPolicy>;
+ using Entry = Table::Entry;
+
+ struct Count: public CountBase {
+ Table table;
+
+ explicit Count(CountType& type) : CountBase(type) { }
+
+ bool init() { return table.init(); }
+ };
+
+ CountTypePtr entryType;
+
+ public:
+ explicit ByUbinodeType(CountTypePtr& entryType)
+ : CountType(),
+ entryType(Move(entryType))
+ { }
+
+ void destructCount(CountBase& countBase) override {
+ Count& count = static_cast<Count&>(countBase);
+ count.~Count();
+ }
+
+ CountBasePtr makeCount() override;
+ void traceCount(CountBase& countBase, JSTracer* trc) override;
+ bool count(CountBase& countBase, mozilla::MallocSizeOf mallocSizeOf, const Node& node) override;
+ bool report(JSContext* cx, CountBase& countBase, MutableHandleValue report) override;
+};
+
+CountBasePtr
+ByUbinodeType::makeCount()
+{
+ UniquePtr<Count> count(js_new<Count>(*this));
+ if (!count || !count->init())
+ return nullptr;
+
+ return CountBasePtr(count.release());
+}
+
+void
+ByUbinodeType::traceCount(CountBase& countBase, JSTracer* trc)
+{
+ Count& count = static_cast<Count&>(countBase);
+ for (Table::Range r = count.table.all(); !r.empty(); r.popFront())
+ r.front().value()->trace(trc);
+}
+
+bool
+ByUbinodeType::count(CountBase& countBase, mozilla::MallocSizeOf mallocSizeOf, const Node& node)
+{
+ Count& count = static_cast<Count&>(countBase);
+
+ const char16_t* key = node.typeName();
+ MOZ_ASSERT(key);
+ Table::AddPtr p = count.table.lookupForAdd(key);
+ if (!p) {
+ CountBasePtr typesCount(entryType->makeCount());
+ if (!typesCount || !count.table.add(p, key, Move(typesCount)))
+ return false;
+ }
+ return p->value()->count(mallocSizeOf, node);
+}
+
+bool
+ByUbinodeType::report(JSContext* cx, CountBase& countBase, MutableHandleValue report)
+{
+ Count& count = static_cast<Count&>(countBase);
+
+ // 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.
+ JS::ubi::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.
+ RootedPlainObject obj(cx, NewBuiltinClassInstance<PlainObject>(cx));
+ if (!obj)
+ return false;
+ for (Entry** entryPtr = entries.begin(); entryPtr < entries.end(); entryPtr++) {
+ Entry& entry = **entryPtr;
+ CountBasePtr& typeCount = entry.value();
+ RootedValue typeReport(cx);
+ if (!typeCount->report(cx, &typeReport))
+ return false;
+
+ const char16_t* name = entry.key();
+ MOZ_ASSERT(name);
+ JSAtom* atom = AtomizeChars(cx, name, js_strlen(name));
+ if (!atom)
+ return false;
+ RootedId entryId(cx, AtomToId(atom));
+
+ if (!DefineProperty(cx, obj, entryId, typeReport))
+ return false;
+ }
+
+ report.setObject(*obj);
+ return true;
+}
+
+
+// A count type that categorizes nodes by the JS stack under which they were
+// allocated.
+class ByAllocationStack : public CountType {
+ using Table = HashMap<StackFrame, CountBasePtr, DefaultHasher<StackFrame>,
+ SystemAllocPolicy>;
+ using Entry = Table::Entry;
+
+ struct Count : public CountBase {
+ // NOTE: You may look up entries in this table by JS::ubi::StackFrame
+ // 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 (with some indirection), 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(CountTypePtr& entryType, CountTypePtr& noStackType)
+ : CountType(),
+ entryType(Move(entryType)),
+ noStackType(Move(noStackType))
+ { }
+
+ void destructCount(CountBase& countBase) override {
+ Count& count = static_cast<Count&>(countBase);
+ count.~Count();
+ }
+
+ CountBasePtr makeCount() override;
+ void traceCount(CountBase& countBase, JSTracer* trc) override;
+ bool count(CountBase& countBase, mozilla::MallocSizeOf mallocSizeOf, const Node& node) override;
+ bool report(JSContext* cx, CountBase& countBase, MutableHandleValue report) override;
+};
+
+CountBasePtr
+ByAllocationStack::makeCount()
+{
+ CountBasePtr noStackCount(noStackType->makeCount());
+ if (!noStackCount)
+ return nullptr;
+
+ UniquePtr<Count> count(js_new<Count>(*this, noStackCount));
+ if (!count || !count->init())
+ return nullptr;
+ return CountBasePtr(count.release());
+}
+
+void
+ByAllocationStack::traceCount(CountBase& countBase, JSTracer* trc)
+{
+ 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 StackFrame that is this entry's key. Do not re-key if
+ // it has moved; see comments for ByAllocationStack::Count::table.
+ const StackFrame* key = &r.front().key();
+ auto& k = *const_cast<StackFrame*>(key);
+ k.trace(trc);
+ }
+ count.noStack->trace(trc);
+}
+
+bool
+ByAllocationStack::count(CountBase& countBase, mozilla::MallocSizeOf mallocSizeOf, const Node& node)
+{
+ Count& count = static_cast<Count&>(countBase);
+
+ // If we do have an allocation stack for this node, include it in the
+ // count for that stack.
+ if (node.hasAllocationStack()) {
+ auto allocationStack = node.allocationStack();
+ auto p = count.table.lookupForAdd(allocationStack);
+ if (!p) {
+ CountBasePtr stackCount(entryType->makeCount());
+ if (!stackCount || !count.table.add(p, allocationStack, Move(stackCount)))
+ return false;
+ }
+ MOZ_ASSERT(p);
+ return p->value()->count(mallocSizeOf, node);
+ }
+
+ // Otherwise, count it in the "no stack" category.
+ return count.noStack->count(mallocSizeOf, node);
+}
+
+bool
+ByAllocationStack::report(JSContext* cx, CountBase& countBase, MutableHandleValue report)
+{
+ Count& count = static_cast<Count&>(countBase);
+
+#ifdef DEBUG
+ // Check that nothing rehashes our table while we hold pointers into it.
+ Generation 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.
+ JS::ubi::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());
+
+ RootedObject stack(cx);
+ if (!entry.key().constructSavedFrameStack(cx, &stack) ||
+ !cx->compartment()->wrap(cx, &stack))
+ {
+ return false;
+ }
+ RootedValue stackVal(cx, ObjectValue(*stack));
+
+ CountBasePtr& stackCount = entry.value();
+ RootedValue stackReport(cx);
+ if (!stackCount->report(cx, &stackReport))
+ return false;
+
+ if (!MapObject::set(cx, map, stackVal, stackReport))
+ return false;
+ }
+
+ if (count.noStack->total_ > 0) {
+ RootedValue noStackReport(cx);
+ if (!count.noStack->report(cx, &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 count type that categorizes nodes by their script's filename.
+class ByFilename : public CountType {
+ using UniqueCString = UniquePtr<char, JS::FreePolicy>;
+
+ struct UniqueCStringHasher {
+ using Lookup = UniqueCString;
+
+ static js::HashNumber hash(const Lookup& lookup) {
+ return CStringHasher::hash(lookup.get());
+ }
+
+ static bool match(const UniqueCString& key, const Lookup& lookup) {
+ return CStringHasher::match(key.get(), lookup.get());
+ }
+ };
+
+ // A table mapping filenames to their counts. Note that we treat scripts
+ // with the same filename as equivalent. If you have several sources with
+ // the same filename, then all their scripts will get bucketed together.
+ using Table = HashMap<UniqueCString, CountBasePtr, UniqueCStringHasher,
+ SystemAllocPolicy>;
+ using Entry = Table::Entry;
+
+ struct Count : public CountBase {
+ Table table;
+ CountBasePtr then;
+ CountBasePtr noFilename;
+
+ Count(CountType& type, CountBasePtr&& then, CountBasePtr&& noFilename)
+ : CountBase(type)
+ , then(Move(then))
+ , noFilename(Move(noFilename))
+ { }
+
+ bool init() { return table.init(); }
+ };
+
+ CountTypePtr thenType;
+ CountTypePtr noFilenameType;
+
+ public:
+ ByFilename(CountTypePtr&& thenType, CountTypePtr&& noFilenameType)
+ : CountType(),
+ thenType(Move(thenType)),
+ noFilenameType(Move(noFilenameType))
+ { }
+
+ void destructCount(CountBase& countBase) override {
+ Count& count = static_cast<Count&>(countBase);
+ count.~Count();
+ }
+
+ CountBasePtr makeCount() override;
+ void traceCount(CountBase& countBase, JSTracer* trc) override;
+ bool count(CountBase& countBase, mozilla::MallocSizeOf mallocSizeOf, const Node& node) override;
+ bool report(JSContext* cx, CountBase& countBase, MutableHandleValue report) override;
+};
+
+CountBasePtr
+ByFilename::makeCount()
+{
+ CountBasePtr thenCount(thenType->makeCount());
+ if (!thenCount)
+ return nullptr;
+
+ CountBasePtr noFilenameCount(noFilenameType->makeCount());
+ if (!noFilenameCount)
+ return nullptr;
+
+ UniquePtr<Count> count(js_new<Count>(*this, Move(thenCount), Move(noFilenameCount)));
+ if (!count || !count->init())
+ return nullptr;
+
+ return CountBasePtr(count.release());
+}
+
+void
+ByFilename::traceCount(CountBase& countBase, JSTracer* trc)
+{
+ Count& count = static_cast<Count&>(countBase);
+ for (Table::Range r = count.table.all(); !r.empty(); r.popFront())
+ r.front().value()->trace(trc);
+ count.noFilename->trace(trc);
+}
+
+bool
+ByFilename::count(CountBase& countBase, mozilla::MallocSizeOf mallocSizeOf, const Node& node)
+{
+ Count& count = static_cast<Count&>(countBase);
+
+ const char* filename = node.scriptFilename();
+ if (!filename)
+ return count.noFilename->count(mallocSizeOf, node);
+
+ UniqueCString myFilename(js_strdup(filename));
+ if (!myFilename)
+ return false;
+
+ Table::AddPtr p = count.table.lookupForAdd(myFilename);
+ if (!p) {
+ CountBasePtr thenCount(thenType->makeCount());
+ if (!thenCount || !count.table.add(p, Move(myFilename), Move(thenCount)))
+ return false;
+ }
+ return p->value()->count(mallocSizeOf, node);
+}
+
+bool
+ByFilename::report(JSContext* cx, CountBase& countBase, MutableHandleValue report)
+{
+ Count& count = static_cast<Count&>(countBase);
+
+ RootedPlainObject obj(cx, countMapToObject(cx, count.table, [](const UniqueCString& key) {
+ return key.get();
+ }));
+ if (!obj)
+ return false;
+
+ RootedValue noFilenameReport(cx);
+ if (!count.noFilename->report(cx, &noFilenameReport) ||
+ !DefineProperty(cx, obj, cx->names().noFilename, noFilenameReport))
+ {
+ return false;
+ }
+
+ report.setObject(*obj);
+ return true;
+}
+
+
+/*** Census Handler *******************************************************************************/
+
+JS_PUBLIC_API(bool)
+CensusHandler::operator() (BreadthFirst<CensusHandler>& traversal,
+ Node origin, const Edge& edge,
+ NodeData* referentData, bool first)
+{
+ // We're only interested in the first time we reach edge.referent, not
+ // in every edge arriving at that node.
+ if (!first)
+ return true;
+
+ // Don't count nodes outside the debuggee zones. Do count things in the
+ // special atoms zone, but don't traverse their outgoing edges, on the
+ // assumption that they are shared resources that debuggee is using.
+ // Symbols are always allocated in the atoms zone, even if they were
+ // created for exactly one compartment and never shared; this rule will
+ // include such nodes in the count.
+ const Node& referent = edge.referent;
+ Zone* zone = referent.zone();
+
+ if (census.targetZones.count() == 0 || census.targetZones.has(zone))
+ return rootCount->count(mallocSizeOf, referent);
+
+ if (zone == census.atomsZone) {
+ traversal.abandonReferent();
+ return rootCount->count(mallocSizeOf, referent);
+ }
+
+ traversal.abandonReferent();
+ return true;
+}
+
+
+/*** Parsing Breakdowns ***************************************************************************/
+
+static CountTypePtr
+ParseChildBreakdown(JSContext* cx, HandleObject breakdown, PropertyName* prop)
+{
+ RootedValue v(cx);
+ if (!GetProperty(cx, breakdown, breakdown, prop, &v))
+ return nullptr;
+ return ParseBreakdown(cx, v);
+}
+
+JS_PUBLIC_API(CountTypePtr)
+ParseBreakdown(JSContext* cx, HandleValue breakdownValue)
+{
+ if (breakdownValue.isUndefined()) {
+ // Construct the default type, { by: 'count' }
+ CountTypePtr simple(cx->new_<SimpleCount>());
+ return simple;
+ }
+
+ RootedObject breakdown(cx, ToObject(cx, breakdownValue));
+ if (!breakdown)
+ return nullptr;
+
+ RootedValue byValue(cx);
+ if (!GetProperty(cx, breakdown, breakdown, cx->names().by, &byValue))
+ return nullptr;
+ RootedString byString(cx, ToString(cx, byValue));
+ if (!byString)
+ return nullptr;
+ RootedLinearString by(cx, byString->ensureLinear(cx));
+ if (!by)
+ return nullptr;
+
+ if (StringEqualsAscii(by, "count")) {
+ RootedValue countValue(cx), bytesValue(cx);
+ if (!GetProperty(cx, breakdown, breakdown, cx->names().count, &countValue) ||
+ !GetProperty(cx, breakdown, breakdown, cx->names().bytes, &bytesValue))
+ return nullptr;
+
+ // Both 'count' and 'bytes' default to true if omitted, but ToBoolean
+ // naturally treats 'undefined' as false; fix this up.
+ if (countValue.isUndefined()) countValue.setBoolean(true);
+ if (bytesValue.isUndefined()) bytesValue.setBoolean(true);
+
+ // Undocumented feature, for testing: { by: 'count' } breakdowns can have
+ // a 'label' property whose value is converted to a string and included as
+ // a 'label' property on the report object.
+ RootedValue label(cx);
+ if (!GetProperty(cx, breakdown, breakdown, cx->names().label, &label))
+ return nullptr;
+
+ UniqueTwoByteChars labelUnique(nullptr);
+ if (!label.isUndefined()) {
+ RootedString labelString(cx, ToString(cx, label));
+ if (!labelString)
+ return nullptr;
+
+ JSFlatString* flat = labelString->ensureFlat(cx);
+ if (!flat)
+ return nullptr;
+
+ AutoStableStringChars chars(cx);
+ if (!chars.initTwoByte(cx, flat))
+ return nullptr;
+
+ // Since flat strings are null-terminated, and AutoStableStringChars
+ // null- terminates if it needs to make a copy, we know that
+ // chars.twoByteChars() is null-terminated.
+ labelUnique = DuplicateString(cx, chars.twoByteChars());
+ if (!labelUnique)
+ return nullptr;
+ }
+
+ CountTypePtr simple(cx->new_<SimpleCount>(labelUnique,
+ ToBoolean(countValue),
+ ToBoolean(bytesValue)));
+ return simple;
+ }
+
+ if (StringEqualsAscii(by, "bucket"))
+ return CountTypePtr(cx->new_<BucketCount>());
+
+ if (StringEqualsAscii(by, "objectClass")) {
+ CountTypePtr thenType(ParseChildBreakdown(cx, breakdown, cx->names().then));
+ if (!thenType)
+ return nullptr;
+
+ CountTypePtr otherType(ParseChildBreakdown(cx, breakdown, cx->names().other));
+ if (!otherType)
+ return nullptr;
+
+ return CountTypePtr(cx->new_<ByObjectClass>(thenType, otherType));
+ }
+
+ if (StringEqualsAscii(by, "coarseType")) {
+ CountTypePtr objectsType(ParseChildBreakdown(cx, breakdown, cx->names().objects));
+ if (!objectsType)
+ return nullptr;
+ CountTypePtr scriptsType(ParseChildBreakdown(cx, breakdown, cx->names().scripts));
+ if (!scriptsType)
+ return nullptr;
+ CountTypePtr stringsType(ParseChildBreakdown(cx, breakdown, cx->names().strings));
+ if (!stringsType)
+ return nullptr;
+ CountTypePtr otherType(ParseChildBreakdown(cx, breakdown, cx->names().other));
+ if (!otherType)
+ return nullptr;
+
+ return CountTypePtr(cx->new_<ByCoarseType>(objectsType,
+ scriptsType,
+ stringsType,
+ otherType));
+ }
+
+ if (StringEqualsAscii(by, "internalType")) {
+ CountTypePtr thenType(ParseChildBreakdown(cx, breakdown, cx->names().then));
+ if (!thenType)
+ return nullptr;
+
+ return CountTypePtr(cx->new_<ByUbinodeType>(thenType));
+ }
+
+ if (StringEqualsAscii(by, "allocationStack")) {
+ CountTypePtr thenType(ParseChildBreakdown(cx, breakdown, cx->names().then));
+ if (!thenType)
+ return nullptr;
+ CountTypePtr noStackType(ParseChildBreakdown(cx, breakdown, cx->names().noStack));
+ if (!noStackType)
+ return nullptr;
+
+ return CountTypePtr(cx->new_<ByAllocationStack>(thenType, noStackType));
+ }
+
+ if (StringEqualsAscii(by, "filename")) {
+ CountTypePtr thenType(ParseChildBreakdown(cx, breakdown, cx->names().then));
+ if (!thenType)
+ return nullptr;
+
+ CountTypePtr noFilenameType(ParseChildBreakdown(cx, breakdown, cx->names().noFilename));
+ if (!noFilenameType)
+ return nullptr;
+
+ return CountTypePtr(cx->new_<ByFilename>(Move(thenType), Move(noFilenameType)));
+ }
+
+ // We didn't recognize the breakdown type; complain.
+ RootedString bySource(cx, ValueToSource(cx, byValue));
+ if (!bySource)
+ return nullptr;
+
+ JSAutoByteString byBytes(cx, bySource);
+ if (!byBytes)
+ return nullptr;
+
+ JS_ReportErrorNumberLatin1(cx, GetErrorMessage, nullptr, JSMSG_DEBUG_CENSUS_BREAKDOWN,
+ byBytes.ptr());
+ return nullptr;
+}
+
+// Get the default census breakdown:
+//
+// { by: "coarseType",
+// objects: { by: "objectClass" },
+// other: { by: "internalType" }
+// }
+static CountTypePtr
+GetDefaultBreakdown(JSContext* cx)
+{
+ CountTypePtr byClass(cx->new_<SimpleCount>());
+ if (!byClass)
+ return nullptr;
+
+ CountTypePtr byClassElse(cx->new_<SimpleCount>());
+ if (!byClassElse)
+ return nullptr;
+
+ CountTypePtr objects(cx->new_<ByObjectClass>(byClass, byClassElse));
+ if (!objects)
+ return nullptr;
+
+ CountTypePtr scripts(cx->new_<SimpleCount>());
+ if (!scripts)
+ return nullptr;
+
+ CountTypePtr strings(cx->new_<SimpleCount>());
+ if (!strings)
+ return nullptr;
+
+ CountTypePtr byType(cx->new_<SimpleCount>());
+ if (!byType)
+ return nullptr;
+
+ CountTypePtr other(cx->new_<ByUbinodeType>(byType));
+ if (!other)
+ return nullptr;
+
+ return CountTypePtr(cx->new_<ByCoarseType>(objects,
+ scripts,
+ strings,
+ other));
+}
+
+JS_PUBLIC_API(bool)
+ParseCensusOptions(JSContext* cx, Census& census, HandleObject options, CountTypePtr& outResult)
+{
+ RootedValue breakdown(cx, UndefinedValue());
+ if (options && !GetProperty(cx, options, options, cx->names().breakdown, &breakdown))
+ return false;
+
+ outResult = breakdown.isUndefined()
+ ? GetDefaultBreakdown(cx)
+ : ParseBreakdown(cx, breakdown);
+ return !!outResult;
+}
+
+} // namespace ubi
+} // namespace JS