/* -*- 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 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 hash policy that compares C strings.
    struct HashPolicy {
        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 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 = HashMap<const char*, CountBasePtr, HashPolicy, SystemAllocPolicy>;
    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;

    // 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& classCount = entry.value();
        RootedValue classReport(cx);
        if (!classCount->report(&classReport))
            return false;

        const char* name = entry.key();
        MOZ_ASSERT(name);
        JSAtom* atom = Atomize(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 (!HasOwnProperty(cx, obj, entryId, &has))
            return false;
        if (has) {
            fprintf(stderr, "already has own property '%s'\n", name);
            MOZ_ASSERT(!has);
        }
#endif

        if (!DefineProperty(cx, obj, entryId, classReport))
            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.
    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<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;
}


/*** 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));
    }

    // 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
