DebuggerMemory.cpp (13145B)
1 /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- 2 * vim: set ts=8 sts=2 et sw=2 tw=80: 3 * This Source Code Form is subject to the terms of the Mozilla Public 4 * License, v. 2.0. If a copy of the MPL was not distributed with this 5 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 6 7 #include "debugger/DebuggerMemory.h" 8 9 #include "jsapi.h" 10 11 #include "builtin/MapObject.h" 12 #include "debugger/Debugger.h" 13 #include "gc/Marking.h" 14 #include "js/AllocPolicy.h" 15 #include "js/Debug.h" 16 #include "js/friend/ErrorMessages.h" // js::GetErrorMessage, JSMSG_* 17 #include "js/PropertySpec.h" 18 #include "js/TracingAPI.h" 19 #include "js/UbiNode.h" 20 #include "js/UbiNodeCensus.h" 21 #include "js/Utility.h" 22 #include "vm/GlobalObject.h" 23 #include "vm/JSContext.h" 24 #include "vm/PlainObject.h" // js::PlainObject 25 #include "vm/Realm.h" 26 #include "vm/SavedStacks.h" 27 28 #include "debugger/Debugger-inl.h" 29 #include "gc/StableCellHasher-inl.h" 30 #include "vm/NativeObject-inl.h" 31 32 using namespace js; 33 34 /* static */ 35 DebuggerMemory* DebuggerMemory::create(JSContext* cx, Debugger* dbg) { 36 Value memoryProtoValue = 37 dbg->object->getReservedSlot(Debugger::JSSLOT_DEBUG_MEMORY_PROTO); 38 RootedObject memoryProto(cx, &memoryProtoValue.toObject()); 39 Rooted<DebuggerMemory*> memory( 40 cx, NewObjectWithGivenProto<DebuggerMemory>(cx, memoryProto)); 41 if (!memory) { 42 return nullptr; 43 } 44 45 dbg->object->setReservedSlot(Debugger::JSSLOT_DEBUG_MEMORY_INSTANCE, 46 ObjectValue(*memory)); 47 memory->setReservedSlot(JSSLOT_DEBUGGER, ObjectValue(*dbg->object)); 48 49 return memory; 50 } 51 52 Debugger* DebuggerMemory::getDebugger() { 53 const Value& dbgVal = getReservedSlot(JSSLOT_DEBUGGER); 54 return Debugger::fromJSObject(&dbgVal.toObject()); 55 } 56 57 /* static */ 58 bool DebuggerMemory::construct(JSContext* cx, unsigned argc, Value* vp) { 59 JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_NO_CONSTRUCTOR, 60 "Debugger.Source"); 61 return false; 62 } 63 64 /* static */ const JSClass DebuggerMemory::class_ = { 65 "Memory", 66 JSCLASS_HAS_RESERVED_SLOTS(JSSLOT_COUNT), 67 }; 68 69 /* static */ 70 DebuggerMemory* DebuggerMemory::checkThis(JSContext* cx, CallArgs& args) { 71 const Value& thisValue = args.thisv(); 72 73 if (!thisValue.isObject()) { 74 JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, 75 JSMSG_OBJECT_REQUIRED, 76 InformalValueTypeName(thisValue)); 77 return nullptr; 78 } 79 80 JSObject& thisObject = thisValue.toObject(); 81 if (!thisObject.is<DebuggerMemory>()) { 82 JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, 83 JSMSG_INCOMPATIBLE_PROTO, class_.name, "method", 84 thisObject.getClass()->name); 85 return nullptr; 86 } 87 88 return &thisObject.as<DebuggerMemory>(); 89 } 90 91 struct MOZ_STACK_CLASS DebuggerMemory::CallData { 92 JSContext* cx; 93 const CallArgs& args; 94 95 Handle<DebuggerMemory*> memory; 96 97 CallData(JSContext* cx, const CallArgs& args, Handle<DebuggerMemory*> memory) 98 : cx(cx), args(args), memory(memory) {} 99 100 // Accessor properties of Debugger.Memory.prototype. 101 102 bool setTrackingAllocationSites(); 103 bool getTrackingAllocationSites(); 104 bool setMaxAllocationsLogLength(); 105 bool getMaxAllocationsLogLength(); 106 bool setAllocationSamplingProbability(); 107 bool getAllocationSamplingProbability(); 108 bool getAllocationsLogOverflowed(); 109 bool getOnGarbageCollection(); 110 bool setOnGarbageCollection(); 111 112 // Function properties of Debugger.Memory.prototype. 113 114 bool takeCensus(); 115 bool drainAllocationsLog(); 116 117 using Method = bool (CallData::*)(); 118 119 template <Method MyMethod> 120 static bool ToNative(JSContext* cx, unsigned argc, Value* vp); 121 }; 122 123 template <DebuggerMemory::CallData::Method MyMethod> 124 /* static */ 125 bool DebuggerMemory::CallData::ToNative(JSContext* cx, unsigned argc, 126 Value* vp) { 127 CallArgs args = CallArgsFromVp(argc, vp); 128 129 Rooted<DebuggerMemory*> memory(cx, DebuggerMemory::checkThis(cx, args)); 130 if (!memory) { 131 return false; 132 } 133 134 CallData data(cx, args, memory); 135 return (data.*MyMethod)(); 136 } 137 138 static bool undefined(const CallArgs& args) { 139 args.rval().setUndefined(); 140 return true; 141 } 142 143 bool DebuggerMemory::CallData::setTrackingAllocationSites() { 144 if (!args.requireAtLeast(cx, "(set trackingAllocationSites)", 1)) { 145 return false; 146 } 147 148 Debugger* dbg = memory->getDebugger(); 149 bool enabling = ToBoolean(args[0]); 150 151 if (enabling == dbg->trackingAllocationSites) { 152 return undefined(args); 153 } 154 155 dbg->trackingAllocationSites = enabling; 156 157 if (enabling) { 158 if (!dbg->addAllocationsTrackingForAllDebuggees(cx)) { 159 dbg->trackingAllocationSites = false; 160 return false; 161 } 162 } else { 163 dbg->removeAllocationsTrackingForAllDebuggees(); 164 } 165 166 return undefined(args); 167 } 168 169 bool DebuggerMemory::CallData::getTrackingAllocationSites() { 170 args.rval().setBoolean(memory->getDebugger()->trackingAllocationSites); 171 return true; 172 } 173 174 bool DebuggerMemory::CallData::drainAllocationsLog() { 175 Debugger* dbg = memory->getDebugger(); 176 177 if (!dbg->trackingAllocationSites) { 178 JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, 179 JSMSG_NOT_TRACKING_ALLOCATIONS, 180 "drainAllocationsLog"); 181 return false; 182 } 183 184 size_t length = dbg->allocationsLog.length(); 185 186 Rooted<ArrayObject*> result(cx, NewDenseFullyAllocatedArray(cx, length)); 187 if (!result) { 188 return false; 189 } 190 result->ensureDenseInitializedLength(0, length); 191 192 for (size_t i = 0; i < length; i++) { 193 Rooted<PlainObject*> obj(cx, NewPlainObject(cx)); 194 if (!obj) { 195 return false; 196 } 197 198 // Don't pop the AllocationsLogEntry yet. The queue's links are followed 199 // by the GC to find the AllocationsLogEntry, but are not barriered, so 200 // we must edit them with great care. Use the queue entry in place, and 201 // then pop and delete together. 202 Debugger::AllocationsLogEntry& entry = dbg->allocationsLog.front(); 203 204 RootedValue frame(cx, ObjectOrNullValue(entry.frame)); 205 if (!DefineDataProperty(cx, obj, cx->names().frame, frame)) { 206 return false; 207 } 208 209 double when = 210 (entry.when - mozilla::TimeStamp::ProcessCreation()).ToMilliseconds(); 211 RootedValue timestampValue(cx, NumberValue(when)); 212 if (!DefineDataProperty(cx, obj, cx->names().timestamp, timestampValue)) { 213 return false; 214 } 215 216 RootedString className( 217 cx, Atomize(cx, entry.className, strlen(entry.className))); 218 if (!className) { 219 return false; 220 } 221 RootedValue classNameValue(cx, StringValue(className)); 222 if (!DefineDataProperty(cx, obj, cx->names().class_, classNameValue)) { 223 return false; 224 } 225 226 RootedValue size(cx, NumberValue(entry.size)); 227 if (!DefineDataProperty(cx, obj, cx->names().size, size)) { 228 return false; 229 } 230 231 RootedValue inNursery(cx, BooleanValue(entry.inNursery)); 232 if (!DefineDataProperty(cx, obj, cx->names().inNursery, inNursery)) { 233 return false; 234 } 235 236 result->setDenseElement(i, ObjectValue(*obj)); 237 238 // Pop the front queue entry, and delete it immediately, so that the GC 239 // sees the AllocationsLogEntry's HeapPtr barriers run atomically with 240 // the change to the graph (the queue link). 241 dbg->allocationsLog.popFront(); 242 } 243 244 dbg->allocationsLogOverflowed = false; 245 args.rval().setObject(*result); 246 return true; 247 } 248 249 bool DebuggerMemory::CallData::getMaxAllocationsLogLength() { 250 args.rval().setInt32(memory->getDebugger()->maxAllocationsLogLength); 251 return true; 252 } 253 254 bool DebuggerMemory::CallData::setMaxAllocationsLogLength() { 255 if (!args.requireAtLeast(cx, "(set maxAllocationsLogLength)", 1)) { 256 return false; 257 } 258 259 int32_t max; 260 if (!ToInt32(cx, args[0], &max)) { 261 return false; 262 } 263 264 if (max < 1) { 265 JS_ReportErrorNumberASCII( 266 cx, GetErrorMessage, nullptr, JSMSG_UNEXPECTED_TYPE, 267 "(set maxAllocationsLogLength)'s parameter", "not a positive integer"); 268 return false; 269 } 270 271 Debugger* dbg = memory->getDebugger(); 272 dbg->maxAllocationsLogLength = max; 273 274 while (dbg->allocationsLog.length() > dbg->maxAllocationsLogLength) { 275 dbg->allocationsLog.popFront(); 276 } 277 278 args.rval().setUndefined(); 279 return true; 280 } 281 282 bool DebuggerMemory::CallData::getAllocationSamplingProbability() { 283 args.rval().setDouble(memory->getDebugger()->allocationSamplingProbability); 284 return true; 285 } 286 287 bool DebuggerMemory::CallData::setAllocationSamplingProbability() { 288 if (!args.requireAtLeast(cx, "(set allocationSamplingProbability)", 1)) { 289 return false; 290 } 291 292 double probability; 293 if (!ToNumber(cx, args[0], &probability)) { 294 return false; 295 } 296 297 // Careful! This must also reject NaN. 298 if (!(0.0 <= probability && probability <= 1.0)) { 299 JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, 300 JSMSG_UNEXPECTED_TYPE, 301 "(set allocationSamplingProbability)'s parameter", 302 "not a number between 0 and 1"); 303 return false; 304 } 305 306 Debugger* dbg = memory->getDebugger(); 307 if (dbg->allocationSamplingProbability != probability) { 308 dbg->allocationSamplingProbability = probability; 309 310 // If this is a change any debuggees would observe, have all debuggee 311 // realms recompute their sampling probabilities. 312 if (dbg->trackingAllocationSites) { 313 for (auto r = dbg->debuggees.all(); !r.empty(); r.popFront()) { 314 r.front()->realm()->chooseAllocationSamplingProbability(); 315 } 316 } 317 } 318 319 args.rval().setUndefined(); 320 return true; 321 } 322 323 bool DebuggerMemory::CallData::getAllocationsLogOverflowed() { 324 args.rval().setBoolean(memory->getDebugger()->allocationsLogOverflowed); 325 return true; 326 } 327 328 bool DebuggerMemory::CallData::getOnGarbageCollection() { 329 return Debugger::getGarbageCollectionHook(cx, args, *memory->getDebugger()); 330 } 331 332 bool DebuggerMemory::CallData::setOnGarbageCollection() { 333 return Debugger::setGarbageCollectionHook(cx, args, *memory->getDebugger()); 334 } 335 336 /* Debugger.Memory.prototype.takeCensus */ 337 338 JS_PUBLIC_API void JS::dbg::SetDebuggerMallocSizeOf( 339 JSContext* cx, mozilla::MallocSizeOf mallocSizeOf) { 340 cx->runtime()->debuggerMallocSizeOf = mallocSizeOf; 341 } 342 343 JS_PUBLIC_API mozilla::MallocSizeOf JS::dbg::GetDebuggerMallocSizeOf( 344 JSContext* cx) { 345 return cx->runtime()->debuggerMallocSizeOf; 346 } 347 348 using JS::ubi::Census; 349 using JS::ubi::CountBasePtr; 350 using JS::ubi::CountTypePtr; 351 352 // The takeCensus function works in three phases: 353 // 354 // 1) We examine the 'breakdown' property of our 'options' argument, and 355 // use that to build a CountType tree. 356 // 357 // 2) We create a count node for the root of our CountType tree, and then walk 358 // the heap, counting each node we find, expanding our tree of counts as we 359 // go. 360 // 361 // 3) We walk the tree of counts and produce JavaScript objects reporting the 362 // accumulated results. 363 bool DebuggerMemory::CallData::takeCensus() { 364 Census census(cx); 365 CountTypePtr rootType; 366 367 RootedObject options(cx); 368 if (args.get(0).isObject()) { 369 options = &args[0].toObject(); 370 } 371 372 if (!JS::ubi::ParseCensusOptions(cx, census, options, rootType)) { 373 return false; 374 } 375 376 JS::ubi::RootedCount rootCount(cx, rootType->makeCount()); 377 if (!rootCount) { 378 ReportOutOfMemory(cx); 379 return false; 380 } 381 JS::ubi::CensusHandler handler(census, rootCount, 382 cx->runtime()->debuggerMallocSizeOf); 383 384 Debugger* dbg = memory->getDebugger(); 385 RootedObject dbgObj(cx, dbg->object); 386 387 // Populate our target set of debuggee zones. 388 for (WeakGlobalObjectSet::Range r = dbg->allDebuggees(); !r.empty(); 389 r.popFront()) { 390 if (!census.targetZones.put(r.front()->zone())) { 391 ReportOutOfMemory(cx); 392 return false; 393 } 394 } 395 396 { 397 JS::ubi::RootList rootList(cx); 398 auto [ok, nogc] = rootList.init(dbgObj); 399 if (!ok) { 400 ReportOutOfMemory(cx); 401 return false; 402 } 403 404 JS::ubi::CensusTraversal traversal(cx, handler, nogc); 405 traversal.wantNames = false; 406 407 if (!traversal.addStart(JS::ubi::Node(&rootList)) || 408 !traversal.traverse()) { 409 ReportOutOfMemory(cx); 410 return false; 411 } 412 } 413 414 return handler.report(cx, args.rval()); 415 } 416 417 /* Debugger.Memory property and method tables. */ 418 419 /* static */ const JSPropertySpec DebuggerMemory::properties[] = { 420 JS_DEBUG_PSGS("trackingAllocationSites", getTrackingAllocationSites, 421 setTrackingAllocationSites), 422 JS_DEBUG_PSGS("maxAllocationsLogLength", getMaxAllocationsLogLength, 423 setMaxAllocationsLogLength), 424 JS_DEBUG_PSGS("allocationSamplingProbability", 425 getAllocationSamplingProbability, 426 setAllocationSamplingProbability), 427 JS_DEBUG_PSG("allocationsLogOverflowed", getAllocationsLogOverflowed), 428 JS_DEBUG_PSGS("onGarbageCollection", getOnGarbageCollection, 429 setOnGarbageCollection), 430 JS_PS_END, 431 }; 432 433 /* static */ const JSFunctionSpec DebuggerMemory::methods[] = { 434 JS_DEBUG_FN("drainAllocationsLog", drainAllocationsLog, 0), 435 JS_DEBUG_FN("takeCensus", takeCensus, 0), 436 JS_FS_END, 437 };