/* -*- 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/MemoryMetrics.h"

#include "mozilla/DebugOnly.h"

#include "jsapi.h"
#include "jscompartment.h"
#include "jsgc.h"
#include "jsobj.h"
#include "jsscript.h"

#include "gc/Heap.h"
#include "jit/BaselineJIT.h"
#include "jit/Ion.h"
#include "vm/ArrayObject.h"
#include "vm/Runtime.h"
#include "vm/Shape.h"
#include "vm/String.h"
#include "vm/Symbol.h"
#include "vm/WrapperObject.h"
#include "wasm/WasmInstance.h"
#include "wasm/WasmJS.h"
#include "wasm/WasmModule.h"

using mozilla::DebugOnly;
using mozilla::MallocSizeOf;
using mozilla::Move;
using mozilla::PodCopy;
using mozilla::PodEqual;

using namespace js;

using JS::RuntimeStats;
using JS::ObjectPrivateVisitor;
using JS::ZoneStats;
using JS::CompartmentStats;

namespace js {

JS_FRIEND_API(size_t)
MemoryReportingSundriesThreshold()
{
    return 8 * 1024;
}

template <typename CharT>
static uint32_t
HashStringChars(JSString* s)
{
    ScopedJSFreePtr<CharT> ownedChars;
    const CharT* chars;
    JS::AutoCheckCannotGC nogc;
    if (s->isLinear()) {
        chars = s->asLinear().chars<CharT>(nogc);
    } else {
        // Slowest hash function evar!
        if (!s->asRope().copyChars<CharT>(/* tcx */ nullptr, ownedChars))
            MOZ_CRASH("oom");
        chars = ownedChars;
    }

    return mozilla::HashString(chars, s->length());
}

/* static */ HashNumber
InefficientNonFlatteningStringHashPolicy::hash(const Lookup& l)
{
    return l->hasLatin1Chars()
           ? HashStringChars<Latin1Char>(l)
           : HashStringChars<char16_t>(l);
}

template <typename Char1, typename Char2>
static bool
EqualStringsPure(JSString* s1, JSString* s2)
{
    if (s1->length() != s2->length())
        return false;

    const Char1* c1;
    ScopedJSFreePtr<Char1> ownedChars1;
    JS::AutoCheckCannotGC nogc;
    if (s1->isLinear()) {
        c1 = s1->asLinear().chars<Char1>(nogc);
    } else {
        if (!s1->asRope().copyChars<Char1>(/* tcx */ nullptr, ownedChars1))
            MOZ_CRASH("oom");
        c1 = ownedChars1;
    }

    const Char2* c2;
    ScopedJSFreePtr<Char2> ownedChars2;
    if (s2->isLinear()) {
        c2 = s2->asLinear().chars<Char2>(nogc);
    } else {
        if (!s2->asRope().copyChars<Char2>(/* tcx */ nullptr, ownedChars2))
            MOZ_CRASH("oom");
        c2 = ownedChars2;
    }

    return EqualChars(c1, c2, s1->length());
}

/* static */ bool
InefficientNonFlatteningStringHashPolicy::match(const JSString* const& k, const Lookup& l)
{
    // We can't use js::EqualStrings, because that flattens our strings.
    JSString* s1 = const_cast<JSString*>(k);
    if (k->hasLatin1Chars()) {
        return l->hasLatin1Chars()
               ? EqualStringsPure<Latin1Char, Latin1Char>(s1, l)
               : EqualStringsPure<Latin1Char, char16_t>(s1, l);
    }

    return l->hasLatin1Chars()
           ? EqualStringsPure<char16_t, Latin1Char>(s1, l)
           : EqualStringsPure<char16_t, char16_t>(s1, l);
}

/* static */ HashNumber
CStringHashPolicy::hash(const Lookup& l)
{
    return mozilla::HashString(l);
}

/* static */ bool
CStringHashPolicy::match(const char* const& k, const Lookup& l)
{
    return strcmp(k, l) == 0;
}

} // namespace js

namespace JS {

NotableStringInfo::NotableStringInfo()
  : StringInfo(),
    buffer(0),
    length(0)
{
}

template <typename CharT>
static void
StoreStringChars(char* buffer, size_t bufferSize, JSString* str)
{
    const CharT* chars;
    ScopedJSFreePtr<CharT> ownedChars;
    JS::AutoCheckCannotGC nogc;
    if (str->isLinear()) {
        chars = str->asLinear().chars<CharT>(nogc);
    } else {
        if (!str->asRope().copyChars<CharT>(/* tcx */ nullptr, ownedChars))
            MOZ_CRASH("oom");
        chars = ownedChars;
    }

    // We might truncate |str| even if it's much shorter than 1024 chars, if
    // |str| contains unicode chars.  Since this is just for a memory reporter,
    // we don't care.
    PutEscapedString(buffer, bufferSize, chars, str->length(), /* quote */ 0);
}

NotableStringInfo::NotableStringInfo(JSString* str, const StringInfo& info)
  : StringInfo(info),
    length(str->length())
{
    size_t bufferSize = Min(str->length() + 1, size_t(MAX_SAVED_CHARS));
    buffer = js_pod_malloc<char>(bufferSize);
    if (!buffer) {
        MOZ_CRASH("oom");
    }

    if (str->hasLatin1Chars())
        StoreStringChars<Latin1Char>(buffer, bufferSize, str);
    else
        StoreStringChars<char16_t>(buffer, bufferSize, str);
}

NotableStringInfo::NotableStringInfo(NotableStringInfo&& info)
  : StringInfo(Move(info)),
    length(info.length)
{
    buffer = info.buffer;
    info.buffer = nullptr;
}

NotableStringInfo& NotableStringInfo::operator=(NotableStringInfo&& info)
{
    MOZ_ASSERT(this != &info, "self-move assignment is prohibited");
    this->~NotableStringInfo();
    new (this) NotableStringInfo(Move(info));
    return *this;
}

NotableClassInfo::NotableClassInfo()
  : ClassInfo(),
    className_(nullptr)
{
}

NotableClassInfo::NotableClassInfo(const char* className, const ClassInfo& info)
  : ClassInfo(info)
{
    size_t bytes = strlen(className) + 1;
    className_ = js_pod_malloc<char>(bytes);
    if (!className_)
        MOZ_CRASH("oom");
    PodCopy(className_, className, bytes);
}

NotableClassInfo::NotableClassInfo(NotableClassInfo&& info)
  : ClassInfo(Move(info))
{
    className_ = info.className_;
    info.className_ = nullptr;
}

NotableClassInfo& NotableClassInfo::operator=(NotableClassInfo&& info)
{
    MOZ_ASSERT(this != &info, "self-move assignment is prohibited");
    this->~NotableClassInfo();
    new (this) NotableClassInfo(Move(info));
    return *this;
}

NotableScriptSourceInfo::NotableScriptSourceInfo()
  : ScriptSourceInfo(),
    filename_(nullptr)
{
}

NotableScriptSourceInfo::NotableScriptSourceInfo(const char* filename, const ScriptSourceInfo& info)
  : ScriptSourceInfo(info)
{
    size_t bytes = strlen(filename) + 1;
    filename_ = js_pod_malloc<char>(bytes);
    if (!filename_)
        MOZ_CRASH("oom");
    PodCopy(filename_, filename, bytes);
}

NotableScriptSourceInfo::NotableScriptSourceInfo(NotableScriptSourceInfo&& info)
  : ScriptSourceInfo(Move(info))
{
    filename_ = info.filename_;
    info.filename_ = nullptr;
}

NotableScriptSourceInfo& NotableScriptSourceInfo::operator=(NotableScriptSourceInfo&& info)
{
    MOZ_ASSERT(this != &info, "self-move assignment is prohibited");
    this->~NotableScriptSourceInfo();
    new (this) NotableScriptSourceInfo(Move(info));
    return *this;
}


} // namespace JS

typedef HashSet<ScriptSource*, DefaultHasher<ScriptSource*>, SystemAllocPolicy> SourceSet;

struct StatsClosure
{
    RuntimeStats* rtStats;
    ObjectPrivateVisitor* opv;
    SourceSet seenSources;
    wasm::Metadata::SeenSet wasmSeenMetadata;
    wasm::ShareableBytes::SeenSet wasmSeenBytes;
    wasm::Table::SeenSet wasmSeenTables;
    bool anonymize;

    StatsClosure(RuntimeStats* rt, ObjectPrivateVisitor* v, bool anon)
      : rtStats(rt),
        opv(v),
        anonymize(anon)
    {}

    bool init() {
        return seenSources.init() &&
               wasmSeenMetadata.init() &&
               wasmSeenBytes.init() &&
               wasmSeenTables.init();
    }
};

static void
DecommittedArenasChunkCallback(JSRuntime* rt, void* data, gc::Chunk* chunk)
{
    // This case is common and fast to check.  Do it first.
    if (chunk->decommittedArenas.isAllClear())
        return;

    size_t n = 0;
    for (size_t i = 0; i < gc::ArenasPerChunk; i++) {
        if (chunk->decommittedArenas.get(i))
            n += gc::ArenaSize;
    }
    MOZ_ASSERT(n > 0);
    *static_cast<size_t*>(data) += n;
}

static void
StatsZoneCallback(JSRuntime* rt, void* data, Zone* zone)
{
    // Append a new CompartmentStats to the vector.
    RuntimeStats* rtStats = static_cast<StatsClosure*>(data)->rtStats;

    // CollectRuntimeStats reserves enough space.
    MOZ_ALWAYS_TRUE(rtStats->zoneStatsVector.growBy(1));
    ZoneStats& zStats = rtStats->zoneStatsVector.back();
    if (!zStats.initStrings(rt))
        MOZ_CRASH("oom");
    rtStats->initExtraZoneStats(zone, &zStats);
    rtStats->currZoneStats = &zStats;

    zone->addSizeOfIncludingThis(rtStats->mallocSizeOf_,
                                 &zStats.typePool,
                                 &zStats.baselineStubsOptimized,
                                 &zStats.uniqueIdMap,
                                 &zStats.shapeTables);
}

static void
StatsCompartmentCallback(JSContext* cx, void* data, JSCompartment* compartment)
{
    // Append a new CompartmentStats to the vector.
    RuntimeStats* rtStats = static_cast<StatsClosure*>(data)->rtStats;

    // CollectRuntimeStats reserves enough space.
    MOZ_ALWAYS_TRUE(rtStats->compartmentStatsVector.growBy(1));
    CompartmentStats& cStats = rtStats->compartmentStatsVector.back();
    if (!cStats.initClasses(cx))
        MOZ_CRASH("oom");
    rtStats->initExtraCompartmentStats(compartment, &cStats);

    compartment->setCompartmentStats(&cStats);

    // Measure the compartment object itself, and things hanging off it.
    compartment->addSizeOfIncludingThis(rtStats->mallocSizeOf_,
                                        &cStats.typeInferenceAllocationSiteTables,
                                        &cStats.typeInferenceArrayTypeTables,
                                        &cStats.typeInferenceObjectTypeTables,
                                        &cStats.compartmentObject,
                                        &cStats.compartmentTables,
                                        &cStats.innerViewsTable,
                                        &cStats.lazyArrayBuffersTable,
                                        &cStats.objectMetadataTable,
                                        &cStats.crossCompartmentWrappersTable,
                                        &cStats.regexpCompartment,
                                        &cStats.savedStacksSet,
                                        &cStats.varNamesSet,
                                        &cStats.nonSyntacticLexicalScopesTable,
                                        &cStats.jitCompartment,
                                        &cStats.privateData);
}

static void
StatsArenaCallback(JSRuntime* rt, void* data, gc::Arena* arena,
                   JS::TraceKind traceKind, size_t thingSize)
{
    RuntimeStats* rtStats = static_cast<StatsClosure*>(data)->rtStats;

    // The admin space includes (a) the header fields and (b) the padding
    // between the end of the header fields and the first GC thing.
    size_t allocationSpace = gc::Arena::thingsSpan(arena->getAllocKind());
    rtStats->currZoneStats->gcHeapArenaAdmin += gc::ArenaSize - allocationSpace;

    // We don't call the callback on unused things.  So we compute the
    // unused space like this:  arenaUnused = maxArenaUnused - arenaUsed.
    // We do this by setting arenaUnused to maxArenaUnused here, and then
    // subtracting thingSize for every used cell, in StatsCellCallback().
    rtStats->currZoneStats->unusedGCThings.addToKind(traceKind, allocationSpace);
}

// FineGrained is used for normal memory reporting.  CoarseGrained is used by
// AddSizeOfTab(), which aggregates all the measurements into a handful of
// high-level numbers, which means that fine-grained reporting would be a waste
// of effort.
enum Granularity {
    FineGrained,
    CoarseGrained
};

static void
AddClassInfo(Granularity granularity, CompartmentStats& cStats, const char* className,
             JS::ClassInfo& info)
{
    if (granularity == FineGrained) {
        if (!className)
            className = "<no class name>";
        CompartmentStats::ClassesHashMap::AddPtr p = cStats.allClasses->lookupForAdd(className);
        if (!p) {
            bool ok = cStats.allClasses->add(p, className, info);
            // Ignore failure -- we just won't record the
            // object/shape/base-shape as notable.
            (void)ok;
        } else {
            p->value().add(info);
        }
    }
}

template <Granularity granularity>
static void
CollectScriptSourceStats(StatsClosure* closure, ScriptSource* ss)
{
    RuntimeStats* rtStats = closure->rtStats;

    SourceSet::AddPtr entry = closure->seenSources.lookupForAdd(ss);
    if (entry)
        return;

    bool ok = closure->seenSources.add(entry, ss);
    (void)ok; // Not much to be done on failure.

    JS::ScriptSourceInfo info;  // This zeroes all the sizes.
    ss->addSizeOfIncludingThis(rtStats->mallocSizeOf_, &info);

    rtStats->runtime.scriptSourceInfo.add(info);

    if (granularity == FineGrained) {
        const char* filename = ss->filename();
        if (!filename)
            filename = "<no filename>";

        JS::RuntimeSizes::ScriptSourcesHashMap::AddPtr p =
            rtStats->runtime.allScriptSources->lookupForAdd(filename);
        if (!p) {
            bool ok = rtStats->runtime.allScriptSources->add(p, filename, info);
            // Ignore failure -- we just won't record the script source as notable.
            (void)ok;
        } else {
            p->value().add(info);
        }
    }
}


// The various kinds of hashing are expensive, and the results are unused when
// doing coarse-grained measurements. Skipping them more than doubles the
// profile speed for complex pages such as gmail.com.
template <Granularity granularity>
static void
StatsCellCallback(JSRuntime* rt, void* data, void* thing, JS::TraceKind traceKind,
                  size_t thingSize)
{
    StatsClosure* closure = static_cast<StatsClosure*>(data);
    RuntimeStats* rtStats = closure->rtStats;
    ZoneStats* zStats = rtStats->currZoneStats;
    switch (traceKind) {
      case JS::TraceKind::Object: {
        JSObject* obj = static_cast<JSObject*>(thing);
        CompartmentStats& cStats = obj->compartment()->compartmentStats();
        JS::ClassInfo info;        // This zeroes all the sizes.
        info.objectsGCHeap += thingSize;

        obj->addSizeOfExcludingThis(rtStats->mallocSizeOf_, &info);

        // These classes require special handling due to shared resources which
        // we must be careful not to report twice.
        if (obj->is<WasmModuleObject>()) {
            wasm::Module& module = obj->as<WasmModuleObject>().module();
            if (ScriptSource* ss = module.metadata().maybeScriptSource())
                CollectScriptSourceStats<granularity>(closure, ss);
            module.addSizeOfMisc(rtStats->mallocSizeOf_,
                                 &closure->wasmSeenMetadata,
                                 &closure->wasmSeenBytes,
                                 &info.objectsNonHeapCodeWasm,
                                 &info.objectsMallocHeapMisc);
        } else if (obj->is<WasmInstanceObject>()) {
            wasm::Instance& instance = obj->as<WasmInstanceObject>().instance();
            if (ScriptSource* ss = instance.metadata().maybeScriptSource())
                CollectScriptSourceStats<granularity>(closure, ss);
            instance.addSizeOfMisc(rtStats->mallocSizeOf_,
                                   &closure->wasmSeenMetadata,
                                   &closure->wasmSeenBytes,
                                   &closure->wasmSeenTables,
                                   &info.objectsNonHeapCodeWasm,
                                   &info.objectsMallocHeapMisc);
        }

        cStats.classInfo.add(info);

        const Class* clasp = obj->getClass();
        const char* className = clasp->name;
        AddClassInfo(granularity, cStats, className, info);

        if (ObjectPrivateVisitor* opv = closure->opv) {
            nsISupports* iface;
            if (opv->getISupports_(obj, &iface) && iface)
                cStats.objectsPrivate += opv->sizeOfIncludingThis(iface);
        }
        break;
      }

      case JS::TraceKind::Script: {
        JSScript* script = static_cast<JSScript*>(thing);
        CompartmentStats& cStats = script->compartment()->compartmentStats();
        cStats.scriptsGCHeap += thingSize;
        cStats.scriptsMallocHeapData += script->sizeOfData(rtStats->mallocSizeOf_);
        cStats.typeInferenceTypeScripts += script->sizeOfTypeScript(rtStats->mallocSizeOf_);
        jit::AddSizeOfBaselineData(script, rtStats->mallocSizeOf_, &cStats.baselineData,
                                   &cStats.baselineStubsFallback);
        cStats.ionData += jit::SizeOfIonData(script, rtStats->mallocSizeOf_);
        CollectScriptSourceStats<granularity>(closure, script->scriptSource());
        break;
      }

      case JS::TraceKind::String: {
        JSString* str = static_cast<JSString*>(thing);

        JS::StringInfo info;
        if (str->hasLatin1Chars()) {
            info.gcHeapLatin1 = thingSize;
            info.mallocHeapLatin1 = str->sizeOfExcludingThis(rtStats->mallocSizeOf_);
        } else {
            info.gcHeapTwoByte = thingSize;
            info.mallocHeapTwoByte = str->sizeOfExcludingThis(rtStats->mallocSizeOf_);
        }
        info.numCopies = 1;

        zStats->stringInfo.add(info);

        // The primary use case for anonymization is automated crash submission
        // (to help detect OOM crashes). In that case, we don't want to pay the
        // memory cost required to do notable string detection.
        if (granularity == FineGrained && !closure->anonymize) {
            ZoneStats::StringsHashMap::AddPtr p = zStats->allStrings->lookupForAdd(str);
            if (!p) {
                bool ok = zStats->allStrings->add(p, str, info);
                // Ignore failure -- we just won't record the string as notable.
                (void)ok;
            } else {
                p->value().add(info);
            }
        }
        break;
      }

      case JS::TraceKind::Symbol:
        zStats->symbolsGCHeap += thingSize;
        break;

      case JS::TraceKind::BaseShape: {
        JS::ShapeInfo info;        // This zeroes all the sizes.
        info.shapesGCHeapBase += thingSize;
        // No malloc-heap measurements.

        zStats->shapeInfo.add(info);
        break;
      }

      case JS::TraceKind::JitCode: {
        zStats->jitCodesGCHeap += thingSize;
        // The code for a script is counted in ExecutableAllocator::sizeOfCode().
        break;
      }

      case JS::TraceKind::LazyScript: {
        LazyScript* lazy = static_cast<LazyScript*>(thing);
        zStats->lazyScriptsGCHeap += thingSize;
        zStats->lazyScriptsMallocHeap += lazy->sizeOfExcludingThis(rtStats->mallocSizeOf_);
        break;
      }

      case JS::TraceKind::Shape: {
        Shape* shape = static_cast<Shape*>(thing);

        JS::ShapeInfo info;        // This zeroes all the sizes.
        if (shape->inDictionary())
            info.shapesGCHeapDict += thingSize;
        else
            info.shapesGCHeapTree += thingSize;
        shape->addSizeOfExcludingThis(rtStats->mallocSizeOf_, &info);
        zStats->shapeInfo.add(info);
        break;
      }

      case JS::TraceKind::ObjectGroup: {
        ObjectGroup* group = static_cast<ObjectGroup*>(thing);
        zStats->objectGroupsGCHeap += thingSize;
        zStats->objectGroupsMallocHeap += group->sizeOfExcludingThis(rtStats->mallocSizeOf_);
        break;
      }

      case JS::TraceKind::Scope: {
        Scope* scope = static_cast<Scope*>(thing);
        zStats->scopesGCHeap += thingSize;
        zStats->scopesMallocHeap += scope->sizeOfExcludingThis(rtStats->mallocSizeOf_);
        break;
      }

      default:
        MOZ_CRASH("invalid traceKind in StatsCellCallback");
    }

    // Yes, this is a subtraction:  see StatsArenaCallback() for details.
    zStats->unusedGCThings.addToKind(traceKind, -thingSize);
}

bool
ZoneStats::initStrings(JSRuntime* rt)
{
    isTotals = false;
    allStrings = rt->new_<StringsHashMap>();
    if (!allStrings || !allStrings->init()) {
        js_delete(allStrings);
        allStrings = nullptr;
        return false;
    }
    return true;
}

bool
CompartmentStats::initClasses(JSRuntime* rt)
{
    isTotals = false;
    allClasses = rt->new_<ClassesHashMap>();
    if (!allClasses || !allClasses->init()) {
        js_delete(allClasses);
        allClasses = nullptr;
        return false;
    }
    return true;
}

static bool
FindNotableStrings(ZoneStats& zStats)
{
    using namespace JS;

    // We should only run FindNotableStrings once per ZoneStats object.
    MOZ_ASSERT(zStats.notableStrings.empty());

    for (ZoneStats::StringsHashMap::Range r = zStats.allStrings->all(); !r.empty(); r.popFront()) {

        JSString* str = r.front().key();
        StringInfo& info = r.front().value();

        if (!info.isNotable())
            continue;

        if (!zStats.notableStrings.growBy(1))
            return false;

        zStats.notableStrings.back() = NotableStringInfo(str, info);

        // We're moving this string from a non-notable to a notable bucket, so
        // subtract it out of the non-notable tallies.
        zStats.stringInfo.subtract(info);
    }
    // Delete |allStrings| now, rather than waiting for zStats's destruction,
    // to reduce peak memory consumption during reporting.
    js_delete(zStats.allStrings);
    zStats.allStrings = nullptr;
    return true;
}

static bool
FindNotableClasses(CompartmentStats& cStats)
{
    using namespace JS;

    // We should only run FindNotableClasses once per ZoneStats object.
    MOZ_ASSERT(cStats.notableClasses.empty());

    for (CompartmentStats::ClassesHashMap::Range r = cStats.allClasses->all();
         !r.empty();
         r.popFront())
    {
        const char* className = r.front().key();
        ClassInfo& info = r.front().value();

        // If this class isn't notable, or if we can't grow the notableStrings
        // vector, skip this string.
        if (!info.isNotable())
            continue;

        if (!cStats.notableClasses.growBy(1))
            return false;

        cStats.notableClasses.back() = NotableClassInfo(className, info);

        // We're moving this class from a non-notable to a notable bucket, so
        // subtract it out of the non-notable tallies.
        cStats.classInfo.subtract(info);
    }
    // Delete |allClasses| now, rather than waiting for zStats's destruction,
    // to reduce peak memory consumption during reporting.
    js_delete(cStats.allClasses);
    cStats.allClasses = nullptr;
    return true;
}

static bool
FindNotableScriptSources(JS::RuntimeSizes& runtime)
{
    using namespace JS;

    // We should only run FindNotableScriptSources once per RuntimeSizes.
    MOZ_ASSERT(runtime.notableScriptSources.empty());

    for (RuntimeSizes::ScriptSourcesHashMap::Range r = runtime.allScriptSources->all();
         !r.empty();
         r.popFront())
    {
        const char* filename = r.front().key();
        ScriptSourceInfo& info = r.front().value();

        if (!info.isNotable())
            continue;

        if (!runtime.notableScriptSources.growBy(1))
            return false;

        runtime.notableScriptSources.back() = NotableScriptSourceInfo(filename, info);

        // We're moving this script source from a non-notable to a notable
        // bucket, so subtract its sizes from the non-notable tallies.
        runtime.scriptSourceInfo.subtract(info);
    }
    // Delete |allScriptSources| now, rather than waiting for zStats's
    // destruction, to reduce peak memory consumption during reporting.
    js_delete(runtime.allScriptSources);
    runtime.allScriptSources = nullptr;
    return true;
}

static bool
CollectRuntimeStatsHelper(JSContext* cx, RuntimeStats* rtStats, ObjectPrivateVisitor* opv,
                          bool anonymize, IterateCellCallback statsCellCallback)
{
    JSRuntime* rt = cx;
    if (!rtStats->compartmentStatsVector.reserve(rt->numCompartments))
        return false;

    if (!rtStats->zoneStatsVector.reserve(rt->gc.zones.length()))
        return false;

    rtStats->gcHeapChunkTotal =
        size_t(JS_GetGCParameter(cx, JSGC_TOTAL_CHUNKS)) * gc::ChunkSize;

    rtStats->gcHeapUnusedChunks =
        size_t(JS_GetGCParameter(cx, JSGC_UNUSED_CHUNKS)) * gc::ChunkSize;

    IterateChunks(cx, &rtStats->gcHeapDecommittedArenas,
                  DecommittedArenasChunkCallback);

    // Take the per-compartment measurements.
    StatsClosure closure(rtStats, opv, anonymize);
    if (!closure.init())
        return false;
    IterateZonesCompartmentsArenasCells(cx, &closure,
                                        StatsZoneCallback,
                                        StatsCompartmentCallback,
                                        StatsArenaCallback,
                                        statsCellCallback);

    // Take the "explicit/js/runtime/" measurements.
    rt->addSizeOfIncludingThis(rtStats->mallocSizeOf_, &rtStats->runtime);

    if (!FindNotableScriptSources(rtStats->runtime))
        return false;

    JS::ZoneStatsVector& zs = rtStats->zoneStatsVector;
    ZoneStats& zTotals = rtStats->zTotals;

    // We don't look for notable strings for zTotals. So we first sum all the
    // zones' measurements to get the totals. Then we find the notable strings
    // within each zone.
    for (size_t i = 0; i < zs.length(); i++)
        zTotals.addSizes(zs[i]);

    for (size_t i = 0; i < zs.length(); i++)
        if (!FindNotableStrings(zs[i]))
            return false;

    MOZ_ASSERT(!zTotals.allStrings);

    JS::CompartmentStatsVector& cs = rtStats->compartmentStatsVector;
    CompartmentStats& cTotals = rtStats->cTotals;

    // As with the zones, we sum all compartments first, and then get the
    // notable classes within each zone.
    for (size_t i = 0; i < cs.length(); i++)
        cTotals.addSizes(cs[i]);

    for (size_t i = 0; i < cs.length(); i++) {
        if (!FindNotableClasses(cs[i]))
            return false;
    }

    MOZ_ASSERT(!cTotals.allClasses);

    rtStats->gcHeapGCThings = rtStats->zTotals.sizeOfLiveGCThings() +
                              rtStats->cTotals.sizeOfLiveGCThings();

#ifdef DEBUG
    // Check that the in-arena measurements look ok.
    size_t totalArenaSize = rtStats->zTotals.gcHeapArenaAdmin +
                            rtStats->zTotals.unusedGCThings.totalSize() +
                            rtStats->gcHeapGCThings;
    MOZ_ASSERT(totalArenaSize % gc::ArenaSize == 0);
#endif

    for (CompartmentsIter comp(rt, WithAtoms); !comp.done(); comp.next())
        comp->nullCompartmentStats();

    size_t numDirtyChunks =
        (rtStats->gcHeapChunkTotal - rtStats->gcHeapUnusedChunks) / gc::ChunkSize;
    size_t perChunkAdmin =
        sizeof(gc::Chunk) - (sizeof(gc::Arena) * gc::ArenasPerChunk);
    rtStats->gcHeapChunkAdmin = numDirtyChunks * perChunkAdmin;

    // |gcHeapUnusedArenas| is the only thing left.  Compute it in terms of
    // all the others.  See the comment in RuntimeStats for explanation.
    rtStats->gcHeapUnusedArenas = rtStats->gcHeapChunkTotal -
                                  rtStats->gcHeapDecommittedArenas -
                                  rtStats->gcHeapUnusedChunks -
                                  rtStats->zTotals.unusedGCThings.totalSize() -
                                  rtStats->gcHeapChunkAdmin -
                                  rtStats->zTotals.gcHeapArenaAdmin -
                                  rtStats->gcHeapGCThings;
    return true;
}

JS_PUBLIC_API(bool)
JS::CollectRuntimeStats(JSContext* cx, RuntimeStats *rtStats, ObjectPrivateVisitor *opv,
                        bool anonymize)
{
    return CollectRuntimeStatsHelper(cx, rtStats, opv, anonymize, StatsCellCallback<FineGrained>);
}

JS_PUBLIC_API(size_t)
JS::SystemCompartmentCount(JSContext* cx)
{
    size_t n = 0;
    for (CompartmentsIter comp(cx, WithAtoms); !comp.done(); comp.next()) {
        if (comp->isSystem())
            ++n;
    }
    return n;
}

JS_PUBLIC_API(size_t)
JS::UserCompartmentCount(JSContext* cx)
{
    size_t n = 0;
    for (CompartmentsIter comp(cx, WithAtoms); !comp.done(); comp.next()) {
        if (!comp->isSystem())
            ++n;
    }
    return n;
}

JS_PUBLIC_API(size_t)
JS::PeakSizeOfTemporary(const JSContext* cx)
{
    return cx->JSRuntime::tempLifoAlloc.peakSizeOfExcludingThis();
}

namespace JS {

class SimpleJSRuntimeStats : public JS::RuntimeStats
{
  public:
    explicit SimpleJSRuntimeStats(MallocSizeOf mallocSizeOf)
      : JS::RuntimeStats(mallocSizeOf)
    {}

    virtual void initExtraZoneStats(JS::Zone* zone, JS::ZoneStats* zStats)
        override
    {}

    virtual void initExtraCompartmentStats(
        JSCompartment* c, JS::CompartmentStats* cStats) override
    {}
};

JS_PUBLIC_API(bool)
AddSizeOfTab(JSContext* cx, HandleObject obj, MallocSizeOf mallocSizeOf, ObjectPrivateVisitor* opv,
             TabSizes* sizes)
{
    SimpleJSRuntimeStats rtStats(mallocSizeOf);

    JS::Zone* zone = GetObjectZone(obj);

    if (!rtStats.compartmentStatsVector.reserve(zone->compartments.length()))
        return false;

    if (!rtStats.zoneStatsVector.reserve(1))
        return false;

    // Take the per-compartment measurements. No need to anonymize because
    // these measurements will be aggregated.
    StatsClosure closure(&rtStats, opv, /* anonymize = */ false);
    if (!closure.init())
        return false;
    IterateZoneCompartmentsArenasCells(cx, zone, &closure,
                                       StatsZoneCallback,
                                       StatsCompartmentCallback,
                                       StatsArenaCallback,
                                       StatsCellCallback<CoarseGrained>);

    MOZ_ASSERT(rtStats.zoneStatsVector.length() == 1);
    rtStats.zTotals.addSizes(rtStats.zoneStatsVector[0]);

    for (size_t i = 0; i < rtStats.compartmentStatsVector.length(); i++)
        rtStats.cTotals.addSizes(rtStats.compartmentStatsVector[i]);

    for (CompartmentsInZoneIter comp(zone); !comp.done(); comp.next())
        comp->nullCompartmentStats();

    rtStats.zTotals.addToTabSizes(sizes);
    rtStats.cTotals.addToTabSizes(sizes);

    return true;
}

JS_PUBLIC_API(bool)
AddServoSizeOf(JSContext* cx, MallocSizeOf mallocSizeOf, ObjectPrivateVisitor *opv,
               ServoSizes *sizes)
{
    SimpleJSRuntimeStats rtStats(mallocSizeOf);

    // No need to anonymize because the results will be aggregated.
    if (!CollectRuntimeStatsHelper(cx, &rtStats, opv, /* anonymize = */ false,
                                   StatsCellCallback<CoarseGrained>))
        return false;

#ifdef DEBUG
    size_t gcHeapTotalOriginal = sizes->gcHeapUsed +
                                 sizes->gcHeapUnused +
                                 sizes->gcHeapAdmin +
                                 sizes->gcHeapDecommitted;
#endif

    rtStats.addToServoSizes(sizes);
    rtStats.zTotals.addToServoSizes(sizes);
    rtStats.cTotals.addToServoSizes(sizes);

#ifdef DEBUG
    size_t gcHeapTotal = sizes->gcHeapUsed +
                         sizes->gcHeapUnused +
                         sizes->gcHeapAdmin +
                         sizes->gcHeapDecommitted;
    MOZ_ASSERT(rtStats.gcHeapChunkTotal == gcHeapTotal - gcHeapTotalOriginal);
#endif

    return true;
}

} // namespace JS