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