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