/* -*- 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 "jsalloc.h" #include "jscntxt.h" #include "jscompartment.h" #include "builtin/MapObject.h" #include "gc/Marking.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/SavedStacks.h" #include "vm/Debugger-inl.h" #include "vm/NativeObject-inl.h" using namespace js; using JS::ubi::BreadthFirst; using JS::ubi::Edge; using JS::ubi::Node; using mozilla::Forward; using mozilla::Maybe; using mozilla::Move; 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 (!DefineProperty(cx, obj, cx->names().frame, frame)) return false; RootedValue timestampValue(cx, NumberValue(entry.when)); if (!DefineProperty(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 (!DefineProperty(cx, obj, cx->names().class_, classNameValue)) return false; RootedValue ctorName(cx, NullValue()); if (entry.ctorName) ctorName.setString(entry.ctorName); if (!DefineProperty(cx, obj, cx->names().constructor, ctorName)) return false; RootedValue size(cx, NumberValue(entry.size)); if (!DefineProperty(cx, obj, cx->names().size, size)) return false; RootedValue inNursery(cx, BooleanValue(entry.inNursery)); if (!DefineProperty(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). if (!dbg->allocationsLog.popFront()) { ReportOutOfMemory(cx); return false; } } 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) { if (!dbg->allocationsLog.popFront()) { ReportOutOfMemory(cx); return false; } } 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->debuggerMallocSizeOf = mallocSizeOf; } JS_PUBLIC_API(mozilla::MallocSizeOf) JS::dbg::GetDebuggerMallocSizeOf(JSContext* cx) { return cx->debuggerMallocSizeOf; } using JS::ubi::Census; using JS::ubi::CountTypePtr; using JS::ubi::CountBasePtr; // 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 };