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

#include "mozilla/Maybe.h"
#include "mozilla/Move.h"
#include "mozilla/Vector.h"

#include <stdlib.h>

#include "builtin/MapObject.h"
#include "gc/Marking.h"
#include "js/AllocPolicy.h"
#include "js/Debug.h"
#include "js/TracingAPI.h"
#include "js/UbiNode.h"
#include "js/UbiNodeCensus.h"
#include "js/Utility.h"
#include "vm/Debugger.h"
#include "vm/GlobalObject.h"
#include "vm/JSCompartment.h"
#include "vm/JSContext.h"
#include "vm/SavedStacks.h"

#include "vm/Debugger-inl.h"
#include "vm/NativeObject-inl.h"

using namespace js;

using mozilla::Forward;
using mozilla::Maybe;
using mozilla::Nothing;

/* static */ DebuggerMemory* DebuggerMemory::create(JSContext* cx,
                                                    Debugger* dbg) {
  Value memoryProtoValue =
      dbg->object->getReservedSlot(Debugger::JSSLOT_DEBUG_MEMORY_PROTO);
  RootedObject memoryProto(cx, &memoryProtoValue.toObject());
  RootedNativeObject memory(
      cx, NewNativeObjectWithGivenProto(cx, &class_, memoryProto));
  if (!memory) return nullptr;

  dbg->object->setReservedSlot(Debugger::JSSLOT_DEBUG_MEMORY_INSTANCE,
                               ObjectValue(*memory));
  memory->setReservedSlot(JSSLOT_DEBUGGER, ObjectValue(*dbg->object));

  return &memory->as<DebuggerMemory>();
}

Debugger* DebuggerMemory::getDebugger() {
  const Value& dbgVal = getReservedSlot(JSSLOT_DEBUGGER);
  return Debugger::fromJSObject(&dbgVal.toObject());
}

/* static */ bool DebuggerMemory::construct(JSContext* cx, unsigned argc,
                                            Value* vp) {
  JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_NO_CONSTRUCTOR,
                            "Debugger.Source");
  return false;
}

/* static */ const Class DebuggerMemory::class_ = {
    "Memory", JSCLASS_HAS_PRIVATE | JSCLASS_HAS_RESERVED_SLOTS(JSSLOT_COUNT)};

/* static */ DebuggerMemory* DebuggerMemory::checkThis(JSContext* cx,
                                                       CallArgs& args,
                                                       const char* fnName) {
  const Value& thisValue = args.thisv();

  if (!thisValue.isObject()) {
    JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
                              JSMSG_NOT_NONNULL_OBJECT,
                              InformalValueTypeName(thisValue));
    return nullptr;
  }

  JSObject& thisObject = thisValue.toObject();
  if (!thisObject.is<DebuggerMemory>()) {
    JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
                              JSMSG_INCOMPATIBLE_PROTO, class_.name, fnName,
                              thisObject.getClass()->name);
    return nullptr;
  }

  // Check for Debugger.Memory.prototype, which has the same class as
  // Debugger.Memory instances, however doesn't actually represent an instance
  // of Debugger.Memory. It is the only object that is<DebuggerMemory>() but
  // doesn't have a Debugger instance.
  if (thisObject.as<DebuggerMemory>()
          .getReservedSlot(JSSLOT_DEBUGGER)
          .isUndefined()) {
    JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
                              JSMSG_INCOMPATIBLE_PROTO, class_.name, fnName,
                              "prototype object");
    return nullptr;
  }

  return &thisObject.as<DebuggerMemory>();
}

/**
 * Get the |DebuggerMemory*| from the current this value and handle any errors
 * that might occur therein.
 *
 * These parameters must already exist when calling this macro:
 * - JSContext* cx
 * - unsigned argc
 * - Value* vp
 * - const char* fnName
 * These parameters will be defined after calling this macro:
 * - CallArgs args
 * - DebuggerMemory* memory (will be non-null)
 */
#define THIS_DEBUGGER_MEMORY(cx, argc, vp, fnName, args, memory)   \
  CallArgs args = CallArgsFromVp(argc, vp);                        \
  Rooted<DebuggerMemory*> memory(cx, checkThis(cx, args, fnName)); \
  if (!memory) return false

static bool undefined(CallArgs& args) {
  args.rval().setUndefined();
  return true;
}

/* static */ bool DebuggerMemory::setTrackingAllocationSites(JSContext* cx,
                                                             unsigned argc,
                                                             Value* vp) {
  THIS_DEBUGGER_MEMORY(cx, argc, vp, "(set trackingAllocationSites)", args,
                       memory);
  if (!args.requireAtLeast(cx, "(set trackingAllocationSites)", 1))
    return false;

  Debugger* dbg = memory->getDebugger();
  bool enabling = ToBoolean(args[0]);

  if (enabling == dbg->trackingAllocationSites) return undefined(args);

  dbg->trackingAllocationSites = enabling;

  if (!dbg->enabled) return undefined(args);

  if (enabling) {
    if (!dbg->addAllocationsTrackingForAllDebuggees(cx)) {
      dbg->trackingAllocationSites = false;
      return false;
    }
  } else {
    dbg->removeAllocationsTrackingForAllDebuggees();
  }

  return undefined(args);
}

/* static */ bool DebuggerMemory::getTrackingAllocationSites(JSContext* cx,
                                                             unsigned argc,
                                                             Value* vp) {
  THIS_DEBUGGER_MEMORY(cx, argc, vp, "(get trackingAllocationSites)", args,
                       memory);
  args.rval().setBoolean(memory->getDebugger()->trackingAllocationSites);
  return true;
}

/* static */ bool DebuggerMemory::drainAllocationsLog(JSContext* cx,
                                                      unsigned argc,
                                                      Value* vp) {
  THIS_DEBUGGER_MEMORY(cx, argc, vp, "drainAllocationsLog", args, memory);
  Debugger* dbg = memory->getDebugger();

  if (!dbg->trackingAllocationSites) {
    JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
                              JSMSG_NOT_TRACKING_ALLOCATIONS,
                              "drainAllocationsLog");
    return false;
  }

  size_t length = dbg->allocationsLog.length();

  RootedArrayObject result(cx, NewDenseFullyAllocatedArray(cx, length));
  if (!result) return false;
  result->ensureDenseInitializedLength(cx, 0, length);

  for (size_t i = 0; i < length; i++) {
    RootedPlainObject obj(cx, NewBuiltinClassInstance<PlainObject>(cx));
    if (!obj) return false;

    // Don't pop the AllocationsLogEntry yet. The queue's links are followed
    // by the GC to find the AllocationsLogEntry, but are not barriered, so
    // we must edit them with great care. Use the queue entry in place, and
    // then pop and delete together.
    Debugger::AllocationsLogEntry& entry = dbg->allocationsLog.front();

    RootedValue frame(cx, ObjectOrNullValue(entry.frame));
    if (!DefineDataProperty(cx, obj, cx->names().frame, frame)) return false;

    double when =
        (entry.when - mozilla::TimeStamp::ProcessCreation()).ToMilliseconds();
    RootedValue timestampValue(cx, NumberValue(when));
    if (!DefineDataProperty(cx, obj, cx->names().timestamp, timestampValue))
      return false;

    RootedString className(
        cx, Atomize(cx, entry.className, strlen(entry.className)));
    if (!className) return false;
    RootedValue classNameValue(cx, StringValue(className));
    if (!DefineDataProperty(cx, obj, cx->names().class_, classNameValue))
      return false;

    RootedValue ctorName(cx, NullValue());
    if (entry.ctorName) ctorName.setString(entry.ctorName);
    if (!DefineDataProperty(cx, obj, cx->names().constructor, ctorName))
      return false;

    RootedValue size(cx, NumberValue(entry.size));
    if (!DefineDataProperty(cx, obj, cx->names().size, size)) return false;

    RootedValue inNursery(cx, BooleanValue(entry.inNursery));
    if (!DefineDataProperty(cx, obj, cx->names().inNursery, inNursery))
      return false;

    result->setDenseElement(i, ObjectValue(*obj));

    // Pop the front queue entry, and delete it immediately, so that the GC
    // sees the AllocationsLogEntry's HeapPtr barriers run atomically with
    // the change to the graph (the queue link).
    dbg->allocationsLog.popFront();
  }

  dbg->allocationsLogOverflowed = false;
  args.rval().setObject(*result);
  return true;
}

/* static */ bool DebuggerMemory::getMaxAllocationsLogLength(JSContext* cx,
                                                             unsigned argc,
                                                             Value* vp) {
  THIS_DEBUGGER_MEMORY(cx, argc, vp, "(get maxAllocationsLogLength)", args,
                       memory);
  args.rval().setInt32(memory->getDebugger()->maxAllocationsLogLength);
  return true;
}

/* static */ bool DebuggerMemory::setMaxAllocationsLogLength(JSContext* cx,
                                                             unsigned argc,
                                                             Value* vp) {
  THIS_DEBUGGER_MEMORY(cx, argc, vp, "(set maxAllocationsLogLength)", args,
                       memory);
  if (!args.requireAtLeast(cx, "(set maxAllocationsLogLength)", 1))
    return false;

  int32_t max;
  if (!ToInt32(cx, args[0], &max)) return false;

  if (max < 1) {
    JS_ReportErrorNumberASCII(
        cx, GetErrorMessage, nullptr, JSMSG_UNEXPECTED_TYPE,
        "(set maxAllocationsLogLength)'s parameter", "not a positive integer");
    return false;
  }

  Debugger* dbg = memory->getDebugger();
  dbg->maxAllocationsLogLength = max;

  while (dbg->allocationsLog.length() > dbg->maxAllocationsLogLength)
    dbg->allocationsLog.popFront();

  args.rval().setUndefined();
  return true;
}

/* static */ bool DebuggerMemory::getAllocationSamplingProbability(
    JSContext* cx, unsigned argc, Value* vp) {
  THIS_DEBUGGER_MEMORY(cx, argc, vp, "(get allocationSamplingProbability)",
                       args, memory);
  args.rval().setDouble(memory->getDebugger()->allocationSamplingProbability);
  return true;
}

/* static */ bool DebuggerMemory::setAllocationSamplingProbability(
    JSContext* cx, unsigned argc, Value* vp) {
  THIS_DEBUGGER_MEMORY(cx, argc, vp, "(set allocationSamplingProbability)",
                       args, memory);
  if (!args.requireAtLeast(cx, "(set allocationSamplingProbability)", 1))
    return false;

  double probability;
  if (!ToNumber(cx, args[0], &probability)) return false;

  // Careful!  This must also reject NaN.
  if (!(0.0 <= probability && probability <= 1.0)) {
    JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
                              JSMSG_UNEXPECTED_TYPE,
                              "(set allocationSamplingProbability)'s parameter",
                              "not a number between 0 and 1");
    return false;
  }

  Debugger* dbg = memory->getDebugger();
  if (dbg->allocationSamplingProbability != probability) {
    dbg->allocationSamplingProbability = probability;

    // If this is a change any debuggees would observe, have all debuggee
    // compartments recompute their sampling probabilities.
    if (dbg->enabled && dbg->trackingAllocationSites) {
      for (auto r = dbg->debuggees.all(); !r.empty(); r.popFront())
        r.front()->compartment()->chooseAllocationSamplingProbability();
    }
  }

  args.rval().setUndefined();
  return true;
}

/* static */ bool DebuggerMemory::getAllocationsLogOverflowed(JSContext* cx,
                                                              unsigned argc,
                                                              Value* vp) {
  THIS_DEBUGGER_MEMORY(cx, argc, vp, "(get allocationsLogOverflowed)", args,
                       memory);
  args.rval().setBoolean(memory->getDebugger()->allocationsLogOverflowed);
  return true;
}

/* static */ bool DebuggerMemory::getOnGarbageCollection(JSContext* cx,
                                                         unsigned argc,
                                                         Value* vp) {
  THIS_DEBUGGER_MEMORY(cx, argc, vp, "(get onGarbageCollection)", args, memory);
  return Debugger::getHookImpl(cx, args, *memory->getDebugger(),
                               Debugger::OnGarbageCollection);
}

/* static */ bool DebuggerMemory::setOnGarbageCollection(JSContext* cx,
                                                         unsigned argc,
                                                         Value* vp) {
  THIS_DEBUGGER_MEMORY(cx, argc, vp, "(set onGarbageCollection)", args, memory);
  return Debugger::setHookImpl(cx, args, *memory->getDebugger(),
                               Debugger::OnGarbageCollection);
}

/* Debugger.Memory.prototype.takeCensus */

JS_PUBLIC_API void JS::dbg::SetDebuggerMallocSizeOf(
    JSContext* cx, mozilla::MallocSizeOf mallocSizeOf) {
  cx->runtime()->debuggerMallocSizeOf = mallocSizeOf;
}

JS_PUBLIC_API mozilla::MallocSizeOf JS::dbg::GetDebuggerMallocSizeOf(
    JSContext* cx) {
  return cx->runtime()->debuggerMallocSizeOf;
}

using JS::ubi::Census;
using JS::ubi::CountBasePtr;
using JS::ubi::CountTypePtr;

// The takeCensus function works in three phases:
//
// 1) We examine the 'breakdown' property of our 'options' argument, and
//    use that to build a CountType tree.
//
// 2) We create a count node for the root of our CountType tree, and then walk
//    the heap, counting each node we find, expanding our tree of counts as we
//    go.
//
// 3) We walk the tree of counts and produce JavaScript objects reporting the
//    accumulated results.
bool DebuggerMemory::takeCensus(JSContext* cx, unsigned argc, Value* vp) {
  THIS_DEBUGGER_MEMORY(cx, argc, vp, "Debugger.Memory.prototype.census", args,
                       memory);

  Census census(cx);
  if (!census.init()) return false;
  CountTypePtr rootType;

  RootedObject options(cx);
  if (args.get(0).isObject()) options = &args[0].toObject();

  if (!JS::ubi::ParseCensusOptions(cx, census, options, rootType)) return false;

  JS::ubi::RootedCount rootCount(cx, rootType->makeCount());
  if (!rootCount) return false;
  JS::ubi::CensusHandler handler(census, rootCount,
                                 cx->runtime()->debuggerMallocSizeOf);

  Debugger* dbg = memory->getDebugger();
  RootedObject dbgObj(cx, dbg->object);

  // Populate our target set of debuggee zones.
  for (WeakGlobalObjectSet::Range r = dbg->allDebuggees(); !r.empty();
       r.popFront()) {
    if (!census.targetZones.put(r.front()->zone())) return false;
  }

  {
    Maybe<JS::AutoCheckCannotGC> maybeNoGC;
    JS::ubi::RootList rootList(cx, maybeNoGC);
    if (!rootList.init(dbgObj)) {
      ReportOutOfMemory(cx);
      return false;
    }

    JS::ubi::CensusTraversal traversal(cx, handler, maybeNoGC.ref());
    if (!traversal.init()) {
      ReportOutOfMemory(cx);
      return false;
    }
    traversal.wantNames = false;

    if (!traversal.addStart(JS::ubi::Node(&rootList)) ||
        !traversal.traverse()) {
      ReportOutOfMemory(cx);
      return false;
    }
  }

  return handler.report(cx, args.rval());
}

/* Debugger.Memory property and method tables. */

/* static */ const JSPropertySpec DebuggerMemory::properties[] = {
    JS_PSGS("trackingAllocationSites", getTrackingAllocationSites,
            setTrackingAllocationSites, 0),
    JS_PSGS("maxAllocationsLogLength", getMaxAllocationsLogLength,
            setMaxAllocationsLogLength, 0),
    JS_PSGS("allocationSamplingProbability", getAllocationSamplingProbability,
            setAllocationSamplingProbability, 0),
    JS_PSG("allocationsLogOverflowed", getAllocationsLogOverflowed, 0),

    JS_PSGS("onGarbageCollection", getOnGarbageCollection,
            setOnGarbageCollection, 0),
    JS_PS_END};

/* static */ const JSFunctionSpec DebuggerMemory::methods[] = {
    JS_FN("drainAllocationsLog", DebuggerMemory::drainAllocationsLog, 0, 0),
    JS_FN("takeCensus", takeCensus, 0, 0), JS_FS_END};
