| /* -*- 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" |
| |
| using namespace js; |
| |
| namespace JS { |
| namespace ubi { |
| |
| 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); |
| } |
| |
| bool |
| Census::init() { |
| AutoLockForExclusiveAccess lock(cx); |
| atomsZone = cx->runtime()->atomsCompartment()->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) |
| { } |
| }; |
| |
| UniquePtr<char16_t[], JS::FreePolicy> label; |
| bool reportCount : 1; |
| bool reportBytes : 1; |
| |
| public: |
| SimpleCount(Census& census, |
| UniquePtr<char16_t[], JS::FreePolicy>& label, |
| bool reportCount=true, |
| bool reportBytes=true) |
| : CountType(census), |
| label(Move(label)), |
| reportCount(reportCount), |
| reportBytes(reportBytes) |
| { } |
| |
| explicit SimpleCount(Census& census) |
| : CountType(census), |
| label(nullptr), |
| reportCount(true), |
| reportBytes(true) |
| { } |
| |
| void destructCount(CountBase& countBase) override { |
| Count& count = static_cast<Count&>(countBase); |
| count.~Count(); |
| } |
| |
| CountBasePtr makeCount() override { return CountBasePtr(census.new_<Count>(*this)); } |
| void traceCount(CountBase& countBase, JSTracer* trc) override { } |
| bool count(CountBase& countBase, const Node& node) override; |
| bool report(CountBase& countBase, MutableHandleValue report) override; |
| }; |
| |
| bool |
| SimpleCount::count(CountBase& countBase, const Node& node) |
| { |
| Count& count = static_cast<Count&>(countBase); |
| count.total_++; |
| if (reportBytes) |
| count.totalBytes_ += node.size(census.cx->runtime()->debuggerMallocSizeOf); |
| return true; |
| } |
| |
| bool |
| SimpleCount::report(CountBase& countBase, MutableHandleValue report) |
| { |
| Count& count = static_cast<Count&>(countBase); |
| |
| RootedPlainObject obj(census.cx, NewBuiltinClassInstance<PlainObject>(census.cx)); |
| if (!obj) |
| return false; |
| |
| RootedValue countValue(census.cx, NumberValue(count.total_)); |
| if (reportCount && !DefineProperty(census.cx, obj, census.cx->names().count, countValue)) |
| return false; |
| |
| RootedValue bytesValue(census.cx, NumberValue(count.totalBytes_)); |
| if (reportBytes && !DefineProperty(census.cx, obj, census.cx->names().bytes, bytesValue)) |
| return false; |
| |
| if (label) { |
| JSString* labelString = JS_NewUCStringCopyZ(census.cx, label.get()); |
| if (!labelString) |
| return false; |
| RootedValue labelValue(census.cx, StringValue(labelString)); |
| if (!DefineProperty(census.cx, obj, census.cx->names().label, labelValue)) |
| return false; |
| } |
| |
| report.setObject(*obj); |
| 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(Census& census, |
| CountTypePtr& objects, |
| CountTypePtr& scripts, |
| CountTypePtr& strings, |
| CountTypePtr& other) |
| : CountType(census), |
| 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, const Node& node) override; |
| bool report(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(census.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, const Node& node) |
| { |
| Count& count = static_cast<Count&>(countBase); |
| count.total_++; |
| |
| switch (node.coarseType()) { |
| case JS::ubi::CoarseType::Object: |
| return count.objects->count(node); |
| case JS::ubi::CoarseType::Script: |
| return count.scripts->count(node); |
| case JS::ubi::CoarseType::String: |
| return count.strings->count(node); |
| case JS::ubi::CoarseType::Other: |
| return count.other->count(node); |
| default: |
| MOZ_CRASH("bad JS::ubi::CoarseType in JS::ubi::ByCoarseType::count"); |
| return false; |
| } |
| } |
| |
| bool |
| ByCoarseType::report(CountBase& countBase, MutableHandleValue report) |
| { |
| Count& count = static_cast<Count&>(countBase); |
| JSContext* cx = census.cx; |
| |
| RootedPlainObject obj(cx, NewBuiltinClassInstance<PlainObject>(cx)); |
| if (!obj) |
| return false; |
| |
| RootedValue objectsReport(cx); |
| if (!count.objects->report(&objectsReport) || |
| !DefineProperty(cx, obj, cx->names().objects, objectsReport)) |
| return false; |
| |
| RootedValue scriptsReport(cx); |
| if (!count.scripts->report(&scriptsReport) || |
| !DefineProperty(cx, obj, cx->names().scripts, scriptsReport)) |
| return false; |
| |
| RootedValue stringsReport(cx); |
| if (!count.strings->report(&stringsReport) || |
| !DefineProperty(cx, obj, cx->names().strings, stringsReport)) |
| return false; |
| |
| RootedValue otherReport(cx); |
| if (!count.other->report(&otherReport) || |
| !DefineProperty(cx, obj, cx->names().other, otherReport)) |
| return false; |
| |
| report.setObject(*obj); |
| return true; |
| } |
| |
| |
| // Comparison function for sorting hash table entries by total count. 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) { |
| size_t lhs = (*static_cast<const Entry* const*>(lhsVoid))->value()->total_; |
| size_t rhs = (*static_cast<const Entry* const*>(rhsVoid))->value()->total_; |
| |
| // qsort sorts in "ascending" order, so we should describe entries with |
| // smaller counts as being "greater than" entries with larger counts. We |
| // don't want to just subtract the counts, as they're unsigned. |
| if (lhs < rhs) |
| return 1; |
| if (lhs > rhs) |
| return -1; |
| return 0; |
| } |
| |
| // A hash policy that compares C strings. |
| struct CStringHashPolicy { |
| using Lookup = const char*; |
| static js::HashNumber hash(Lookup l) { return mozilla::HashString(l); } |
| static bool match(const char* key, Lookup lookup) { |
| return strcmp(key, lookup) == 0; |
| } |
| }; |
| |
| // A hash map mapping from C strings to counts. |
| using CStringCountMap = HashMap<const char*, CountBasePtr, CStringHashPolicy, SystemAllocPolicy>; |
| |
| // Convert a CStringCountMap into an object with each key one of the c strings |
| // from the map and each value the associated count's report. For use with |
| // reporting. |
| static PlainObject* |
| cStringCountMapToObject(JSContext* cx, CStringCountMap& map) { |
| // 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<CStringCountMap::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<CStringCountMap::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(&thenReport)) |
| return nullptr; |
| |
| const char* name = 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(Census& census, |
| CountTypePtr& classesType, |
| CountTypePtr& otherType) |
| : CountType(census), |
| 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, const Node& node) override; |
| bool report(CountBase& countBase, MutableHandleValue report) override; |
| }; |
| |
| CountBasePtr |
| ByObjectClass::makeCount() |
| { |
| CountBasePtr otherCount(otherType->makeCount()); |
| if (!otherCount) |
| return nullptr; |
| |
| UniquePtr<Count> count(census.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, const Node& node) |
| { |
| Count& count = static_cast<Count&>(countBase); |
| count.total_++; |
| |
| const char* className = node.jsObjectClassName(); |
| if (!className) |
| return count.other->count(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(node); |
| } |
| |
| bool |
| ByObjectClass::report(CountBase& countBase, MutableHandleValue report) |
| { |
| Count& count = static_cast<Count&>(countBase); |
| JSContext* cx = census.cx; |
| |
| RootedPlainObject obj(cx, cStringCountMapToObject(cx, count.table)); |
| if (!obj) |
| return false; |
| |
| RootedValue otherReport(cx); |
| if (!count.other->report(&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: |
| ByUbinodeType(Census& census, CountTypePtr& entryType) |
| : CountType(census), |
| 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, const Node& node) override; |
| bool report(CountBase& countBase, MutableHandleValue report) override; |
| }; |
| |
| CountBasePtr |
| ByUbinodeType::makeCount() |
| { |
| UniquePtr<Count> count(census.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, const Node& node) |
| { |
| Count& count = static_cast<Count&>(countBase); |
| count.total_++; |
| |
| 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(node); |
| } |
| |
| bool |
| ByUbinodeType::report(CountBase& countBase, MutableHandleValue report) |
| { |
| Count& count = static_cast<Count&>(countBase); |
| JSContext* cx = census.cx; |
| |
| // 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<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(&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(Census& census, CountTypePtr& entryType, CountTypePtr& noStackType) |
| : CountType(census), |
| 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, const Node& node) override; |
| bool report(CountBase& countBase, MutableHandleValue report) override; |
| }; |
| |
| CountBasePtr |
| ByAllocationStack::makeCount() |
| { |
| CountBasePtr noStackCount(noStackType->makeCount()); |
| if (!noStackCount) |
| return nullptr; |
| |
| UniquePtr<Count> count(census.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, const Node& node) |
| { |
| Count& count = static_cast<Count&>(countBase); |
| count.total_++; |
| |
| // 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(node); |
| } |
| |
| // Otherwise, count it in the "no stack" category. |
| return count.noStack->count(node); |
| } |
| |
| bool |
| ByAllocationStack::report(CountBase& countBase, MutableHandleValue report) |
| { |
| Count& count = static_cast<Count&>(countBase); |
| JSContext* cx = census.cx; |
| |
| #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. |
| mozilla::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(&stackReport)) |
| return false; |
| |
| if (!MapObject::set(cx, map, stackVal, stackReport)) |
| return false; |
| } |
| |
| if (count.noStack->total_ > 0) { |
| 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 count type that categorizes nodes by their script's filename. |
| class ByFilename : public CountType { |
| // 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 = CStringCountMap; |
| 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(Census& census, |
| CountTypePtr& thenType, |
| CountTypePtr& noFilenameType) |
| : CountType(census), |
| 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, const Node& node) override; |
| bool report(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(census.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, const Node& node) |
| { |
| Count& count = static_cast<Count&>(countBase); |
| count.total_++; |
| |
| const char* filename = node.scriptFilename(); |
| if (!filename) |
| return count.noFilename->count(node); |
| |
| Table::AddPtr p = count.table.lookupForAdd(filename); |
| if (!p) { |
| CountBasePtr thenCount(thenType->makeCount()); |
| if (!thenCount || !count.table.add(p, filename, Move(thenCount))) |
| return false; |
| } |
| return p->value()->count(node); |
| } |
| |
| bool |
| ByFilename::report(CountBase& countBase, MutableHandleValue report) |
| { |
| Count& count = static_cast<Count&>(countBase); |
| JSContext* cx = census.cx; |
| |
| RootedPlainObject obj(cx, cStringCountMapToObject(cx, count.table)); |
| if (!obj) |
| return false; |
| |
| RootedValue noFilenameReport(cx); |
| if (!count.noFilename->report(&noFilenameReport) || |
| !DefineProperty(cx, obj, cx->names().noFilename, noFilenameReport)) |
| { |
| return false; |
| } |
| |
| report.setObject(*obj); |
| return true; |
| } |
| |
| |
| /*** Census Handler *******************************************************************************/ |
| |
| 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(referent); |
| |
| if (zone == census.atomsZone) { |
| traversal.abandonReferent(); |
| return rootCount->count(referent); |
| } |
| |
| traversal.abandonReferent(); |
| return true; |
| } |
| |
| |
| /*** Parsing Breakdowns ***************************************************************************/ |
| |
| static CountTypePtr ParseBreakdown(Census& census, HandleValue breakdownValue); |
| |
| static CountTypePtr |
| ParseChildBreakdown(Census& census, HandleObject breakdown, PropertyName* prop) |
| { |
| JSContext* cx = census.cx; |
| RootedValue v(cx); |
| if (!GetProperty(cx, breakdown, breakdown, prop, &v)) |
| return nullptr; |
| return ParseBreakdown(census, v); |
| } |
| |
| static CountTypePtr |
| ParseBreakdown(Census& census, HandleValue breakdownValue) |
| { |
| JSContext* cx = census.cx; |
| |
| if (breakdownValue.isUndefined()) { |
| // Construct the default type, { by: 'count' } |
| CountTypePtr simple(census.new_<SimpleCount>(census)); |
| 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; |
| |
| UniquePtr<char16_t[], JS::FreePolicy> 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(census.new_<SimpleCount>(census, |
| labelUnique, |
| ToBoolean(countValue), |
| ToBoolean(bytesValue))); |
| return simple; |
| } |
| |
| if (StringEqualsAscii(by, "objectClass")) { |
| CountTypePtr thenType(ParseChildBreakdown(census, breakdown, cx->names().then)); |
| if (!thenType) |
| return nullptr; |
| |
| CountTypePtr otherType(ParseChildBreakdown(census, breakdown, cx->names().other)); |
| if (!otherType) |
| return nullptr; |
| |
| return CountTypePtr(census.new_<ByObjectClass>(census, thenType, otherType)); |
| } |
| |
| if (StringEqualsAscii(by, "coarseType")) { |
| CountTypePtr objectsType(ParseChildBreakdown(census, breakdown, cx->names().objects)); |
| if (!objectsType) |
| return nullptr; |
| CountTypePtr scriptsType(ParseChildBreakdown(census, breakdown, cx->names().scripts)); |
| if (!scriptsType) |
| return nullptr; |
| CountTypePtr stringsType(ParseChildBreakdown(census, breakdown, cx->names().strings)); |
| if (!stringsType) |
| return nullptr; |
| CountTypePtr otherType(ParseChildBreakdown(census, breakdown, cx->names().other)); |
| if (!otherType) |
| return nullptr; |
| |
| return CountTypePtr(census.new_<ByCoarseType>(census, |
| objectsType, |
| scriptsType, |
| stringsType, |
| otherType)); |
| } |
| |
| if (StringEqualsAscii(by, "internalType")) { |
| CountTypePtr thenType(ParseChildBreakdown(census, breakdown, cx->names().then)); |
| if (!thenType) |
| return nullptr; |
| |
| return CountTypePtr(census.new_<ByUbinodeType>(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_<ByAllocationStack>(census, thenType, noStackType)); |
| } |
| |
| if (StringEqualsAscii(by, "filename")) { |
| CountTypePtr thenType(ParseChildBreakdown(census, breakdown, cx->names().then)); |
| if (!thenType) |
| return nullptr; |
| |
| CountTypePtr noFilenameType(ParseChildBreakdown(census, breakdown, cx->names().noFilename)); |
| if (!noFilenameType) |
| return nullptr; |
| |
| return CountTypePtr(census.new_<ByFilename>(census, thenType, 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_ReportErrorNumber(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(Census& census) |
| { |
| CountTypePtr byClass(census.new_<SimpleCount>(census)); |
| CountTypePtr byClassElse(census.new_<SimpleCount>(census)); |
| CountTypePtr objects(census.new_<ByObjectClass>(census, |
| byClass, |
| byClassElse)); |
| |
| CountTypePtr scripts(census.new_<SimpleCount>(census)); |
| CountTypePtr strings(census.new_<SimpleCount>(census)); |
| |
| CountTypePtr byType(census.new_<SimpleCount>(census)); |
| CountTypePtr other(census.new_<ByUbinodeType>(census, byType)); |
| |
| return CountTypePtr(census.new_<ByCoarseType>(census, |
| objects, |
| scripts, |
| strings, |
| other)); |
| } |
| |
| 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(census) |
| : ParseBreakdown(census, breakdown); |
| return !!outResult; |
| } |
| |
| } // namespace ubi |
| } // namespace JS |