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 "vm/GeckoProfiler-inl.h"
8 
9 #include "mozilla/ArrayUtils.h"
10 #include "mozilla/DebugOnly.h"
11 #include "mozilla/Sprintf.h"
12 
13 #include "jsnum.h"
14 
15 #include "gc/GC.h"
16 #include "gc/PublicIterators.h"
17 #include "jit/BaselineFrame.h"
18 #include "jit/BaselineJIT.h"
19 #include "jit/JitcodeMap.h"
20 #include "jit/JitFrames.h"
21 #include "jit/JitRealm.h"
22 #include "jit/JSJitFrameIter.h"
23 #include "js/TraceLoggerAPI.h"
24 #include "util/StringBuffer.h"
25 #include "vm/FrameIter.h"  // js::OnlyJSJitFrameIter
26 #include "vm/JSScript.h"
27 
28 #include "gc/Marking-inl.h"
29 #include "vm/JSScript-inl.h"
30 
31 using namespace js;
32 
33 using mozilla::DebugOnly;
34 
GeckoProfilerThread()35 GeckoProfilerThread::GeckoProfilerThread()
36     : profilingStack_(nullptr), profilingStackIfEnabled_(nullptr) {}
37 
GeckoProfilerRuntime(JSRuntime * rt)38 GeckoProfilerRuntime::GeckoProfilerRuntime(JSRuntime* rt)
39     : rt(rt),
40       strings_(),
41       slowAssertions(false),
42       enabled_(false),
43       eventMarker_(nullptr) {
44   MOZ_ASSERT(rt != nullptr);
45 }
46 
setProfilingStack(ProfilingStack * profilingStack,bool enabled)47 void GeckoProfilerThread::setProfilingStack(ProfilingStack* profilingStack,
48                                             bool enabled) {
49   profilingStack_ = profilingStack;
50   profilingStackIfEnabled_ = enabled ? profilingStack : nullptr;
51 }
52 
setEventMarker(void (* fn)(const char *))53 void GeckoProfilerRuntime::setEventMarker(void (*fn)(const char*)) {
54   eventMarker_ = fn;
55 }
56 
57 // Get a pointer to the top-most profiling frame, given the exit frame pointer.
GetTopProfilingJitFrame(Activation * act)58 static void* GetTopProfilingJitFrame(Activation* act) {
59   if (!act || !act->isJit()) {
60     return nullptr;
61   }
62 
63   jit::JitActivation* jitActivation = act->asJit();
64 
65   // If there is no exit frame set, just return.
66   if (!jitActivation->hasExitFP()) {
67     return nullptr;
68   }
69 
70   // Skip wasm frames that might be in the way.
71   OnlyJSJitFrameIter iter(jitActivation);
72   if (iter.done()) {
73     return nullptr;
74   }
75 
76   jit::JSJitProfilingFrameIterator jitIter(
77       (jit::CommonFrameLayout*)iter.frame().fp());
78   MOZ_ASSERT(!jitIter.done());
79   return jitIter.fp();
80 }
81 
enable(bool enabled)82 void GeckoProfilerRuntime::enable(bool enabled) {
83   JSContext* cx = rt->mainContextFromAnyThread();
84   MOZ_ASSERT(cx->geckoProfiler().infraInstalled());
85 
86   if (enabled_ == enabled) {
87     return;
88   }
89 
90   /*
91    * Ensure all future generated code will be instrumented, or that all
92    * currently instrumented code is discarded
93    */
94   ReleaseAllJITCode(rt->defaultFreeOp());
95 
96   // This function is called when the Gecko profiler makes a new Sampler
97   // (and thus, a new circular buffer). Set all current entries in the
98   // JitcodeGlobalTable as expired and reset the buffer range start.
99   if (rt->hasJitRuntime() && rt->jitRuntime()->hasJitcodeGlobalTable()) {
100     rt->jitRuntime()->getJitcodeGlobalTable()->setAllEntriesAsExpired();
101   }
102   rt->setProfilerSampleBufferRangeStart(0);
103 
104   // Ensure that lastProfilingFrame is null for the main thread.
105   if (cx->jitActivation) {
106     cx->jitActivation->setLastProfilingFrame(nullptr);
107     cx->jitActivation->setLastProfilingCallSite(nullptr);
108   }
109 
110   // Reset the tracelogger, if toggled on
111   JS::ResetTraceLogger();
112 
113   enabled_ = enabled;
114 
115   /* Toggle Gecko Profiler-related jumps on baseline jitcode.
116    * The call to |ReleaseAllJITCode| above will release most baseline jitcode,
117    * but not jitcode for scripts with active frames on the stack.  These scripts
118    * need to have their profiler state toggled so they behave properly.
119    */
120   jit::ToggleBaselineProfiling(cx, enabled);
121 
122   // Update lastProfilingFrame to point to the top-most JS jit-frame currently
123   // on stack.
124   if (cx->jitActivation) {
125     // Walk through all activations, and set their lastProfilingFrame
126     // appropriately.
127     if (enabled) {
128       Activation* act = cx->activation();
129       void* lastProfilingFrame = GetTopProfilingJitFrame(act);
130 
131       jit::JitActivation* jitActivation = cx->jitActivation;
132       while (jitActivation) {
133         jitActivation->setLastProfilingFrame(lastProfilingFrame);
134         jitActivation->setLastProfilingCallSite(nullptr);
135 
136         jitActivation = jitActivation->prevJitActivation();
137         lastProfilingFrame = GetTopProfilingJitFrame(jitActivation);
138       }
139     } else {
140       jit::JitActivation* jitActivation = cx->jitActivation;
141       while (jitActivation) {
142         jitActivation->setLastProfilingFrame(nullptr);
143         jitActivation->setLastProfilingCallSite(nullptr);
144         jitActivation = jitActivation->prevJitActivation();
145       }
146     }
147   }
148 
149   // WebAssembly code does not need to be released, but profiling string
150   // labels have to be generated so that they are available during async
151   // profiling stack iteration.
152   for (RealmsIter r(rt); !r.done(); r.next()) {
153     r->wasm.ensureProfilingLabels(enabled);
154   }
155 
156 #ifdef JS_STRUCTURED_SPEW
157   // Enable the structured spewer if the environment variable is set.
158   if (enabled) {
159     cx->spewer().enableSpewing();
160   } else {
161     cx->spewer().disableSpewing();
162   }
163 #endif
164 }
165 
166 /* Lookup the string for the function/script, creating one if necessary */
profileString(JSContext * cx,BaseScript * script)167 const char* GeckoProfilerRuntime::profileString(JSContext* cx,
168                                                 BaseScript* script) {
169   ProfileStringMap::AddPtr s = strings().lookupForAdd(script);
170 
171   if (!s) {
172     UniqueChars str = allocProfileString(cx, script);
173     if (!str) {
174       return nullptr;
175     }
176     MOZ_ASSERT(script->hasBytecode());
177     if (!strings().add(s, script, std::move(str))) {
178       ReportOutOfMemory(cx);
179       return nullptr;
180     }
181   }
182 
183   return s->value().get();
184 }
185 
onScriptFinalized(BaseScript * script)186 void GeckoProfilerRuntime::onScriptFinalized(BaseScript* script) {
187   /*
188    * This function is called whenever a script is destroyed, regardless of
189    * whether profiling has been turned on, so don't invoke a function on an
190    * invalid hash set. Also, even if profiling was enabled but then turned
191    * off, we still want to remove the string, so no check of enabled() is
192    * done.
193    */
194   if (ProfileStringMap::Ptr entry = strings().lookup(script)) {
195     strings().remove(entry);
196   }
197 }
198 
markEvent(const char * event)199 void GeckoProfilerRuntime::markEvent(const char* event) {
200   MOZ_ASSERT(enabled());
201   if (eventMarker_) {
202     JS::AutoSuppressGCAnalysis nogc;
203     eventMarker_(event);
204   }
205 }
206 
enter(JSContext * cx,JSScript * script)207 bool GeckoProfilerThread::enter(JSContext* cx, JSScript* script) {
208   const char* dynamicString =
209       cx->runtime()->geckoProfiler().profileString(cx, script);
210   if (dynamicString == nullptr) {
211     return false;
212   }
213 
214 #ifdef DEBUG
215   // In debug builds, assert the JS profiling stack frames already on the
216   // stack have a non-null pc. Only look at the top frames to avoid quadratic
217   // behavior.
218   uint32_t sp = profilingStack_->stackPointer;
219   if (sp > 0 && sp - 1 < profilingStack_->stackCapacity()) {
220     size_t start = (sp > 4) ? sp - 4 : 0;
221     for (size_t i = start; i < sp - 1; i++) {
222       MOZ_ASSERT_IF(profilingStack_->frames[i].isJsFrame(),
223                     profilingStack_->frames[i].pc());
224     }
225   }
226 #endif
227 
228   profilingStack_->pushJsFrame(
229       "", dynamicString, script, script->code(),
230       script->realm()->creationOptions().profilerRealmID());
231   return true;
232 }
233 
exit(JSContext * cx,JSScript * script)234 void GeckoProfilerThread::exit(JSContext* cx, JSScript* script) {
235   profilingStack_->pop();
236 
237 #ifdef DEBUG
238   /* Sanity check to make sure push/pop balanced */
239   uint32_t sp = profilingStack_->stackPointer;
240   if (sp < profilingStack_->stackCapacity()) {
241     JSRuntime* rt = script->runtimeFromMainThread();
242     const char* dynamicString = rt->geckoProfiler().profileString(cx, script);
243     /* Can't fail lookup because we should already be in the set */
244     MOZ_ASSERT(dynamicString);
245 
246     // Bug 822041
247     if (!profilingStack_->frames[sp].isJsFrame()) {
248       fprintf(stderr, "--- ABOUT TO FAIL ASSERTION ---\n");
249       fprintf(stderr, " frames=%p size=%u/%u\n", (void*)profilingStack_->frames,
250               uint32_t(profilingStack_->stackPointer),
251               profilingStack_->stackCapacity());
252       for (int32_t i = sp; i >= 0; i--) {
253         ProfilingStackFrame& frame = profilingStack_->frames[i];
254         if (frame.isJsFrame()) {
255           fprintf(stderr, "  [%d] JS %s\n", i, frame.dynamicString());
256         } else {
257           fprintf(stderr, "  [%d] Label %s\n", i, frame.dynamicString());
258         }
259       }
260     }
261 
262     ProfilingStackFrame& frame = profilingStack_->frames[sp];
263     MOZ_ASSERT(frame.isJsFrame());
264     MOZ_ASSERT(frame.script() == script);
265     MOZ_ASSERT(strcmp((const char*)frame.dynamicString(), dynamicString) == 0);
266   }
267 #endif
268 }
269 
270 /*
271  * Serializes the script/function pair into a "descriptive string" which is
272  * allowed to fail. This function cannot trigger a GC because it could finalize
273  * some scripts, resize the hash table of profile strings, and invalidate the
274  * AddPtr held while invoking allocProfileString.
275  */
276 /* static */
allocProfileString(JSContext * cx,BaseScript * script)277 UniqueChars GeckoProfilerRuntime::allocProfileString(JSContext* cx,
278                                                      BaseScript* script) {
279   // Note: this profiler string is regexp-matched by
280   // devtools/client/profiler/cleopatra/js/parserWorker.js.
281 
282   // If the script has a function, try calculating its name.
283   bool hasName = false;
284   size_t nameLength = 0;
285   UniqueChars nameStr;
286   JSFunction* func = script->function();
287   if (func && func->displayAtom()) {
288     nameStr = StringToNewUTF8CharsZ(cx, *func->displayAtom());
289     if (!nameStr) {
290       return nullptr;
291     }
292 
293     nameLength = strlen(nameStr.get());
294     hasName = true;
295   }
296 
297   // Calculate filename length. We cap this to a reasonable limit to avoid
298   // performance impact of strlen/alloc/memcpy.
299   constexpr size_t MaxFilenameLength = 200;
300   const char* filenameStr = script->filename() ? script->filename() : "(null)";
301   size_t filenameLength = js_strnlen(filenameStr, MaxFilenameLength);
302 
303   // Calculate line + column length.
304   bool hasLineAndColumn = false;
305   size_t lineAndColumnLength = 0;
306   char lineAndColumnStr[30];
307   if (hasName || script->isFunction() || script->isForEval()) {
308     lineAndColumnLength = SprintfLiteral(lineAndColumnStr, "%u:%u",
309                                          script->lineno(), script->column());
310     hasLineAndColumn = true;
311   }
312 
313   // Full profile string for scripts with functions is:
314   //      FuncName (FileName:Lineno:Column)
315   // Full profile string for scripts without functions is:
316   //      FileName:Lineno:Column
317   // Full profile string for scripts without functions and without lines is:
318   //      FileName
319 
320   // Calculate full string length.
321   size_t fullLength = 0;
322   if (hasName) {
323     MOZ_ASSERT(hasLineAndColumn);
324     fullLength = nameLength + 2 + filenameLength + 1 + lineAndColumnLength + 1;
325   } else if (hasLineAndColumn) {
326     fullLength = filenameLength + 1 + lineAndColumnLength;
327   } else {
328     fullLength = filenameLength;
329   }
330 
331   // Allocate string.
332   UniqueChars str(cx->pod_malloc<char>(fullLength + 1));
333   if (!str) {
334     return nullptr;
335   }
336 
337   size_t cur = 0;
338 
339   // Fill string with function name if needed.
340   if (hasName) {
341     memcpy(str.get() + cur, nameStr.get(), nameLength);
342     cur += nameLength;
343     str[cur++] = ' ';
344     str[cur++] = '(';
345   }
346 
347   // Fill string with filename chars.
348   memcpy(str.get() + cur, filenameStr, filenameLength);
349   cur += filenameLength;
350 
351   // Fill line + column chars.
352   if (hasLineAndColumn) {
353     str[cur++] = ':';
354     memcpy(str.get() + cur, lineAndColumnStr, lineAndColumnLength);
355     cur += lineAndColumnLength;
356   }
357 
358   // Terminal ')' if necessary.
359   if (hasName) {
360     str[cur++] = ')';
361   }
362 
363   MOZ_ASSERT(cur == fullLength);
364   str[cur] = 0;
365 
366   return str;
367 }
368 
trace(JSTracer * trc)369 void GeckoProfilerThread::trace(JSTracer* trc) {
370   if (profilingStack_) {
371     size_t size = profilingStack_->stackSize();
372     for (size_t i = 0; i < size; i++) {
373       profilingStack_->frames[i].trace(trc);
374     }
375   }
376 }
377 
fixupStringsMapAfterMovingGC()378 void GeckoProfilerRuntime::fixupStringsMapAfterMovingGC() {
379   for (ProfileStringMap::Enum e(strings()); !e.empty(); e.popFront()) {
380     BaseScript* script = e.front().key();
381     if (IsForwarded(script)) {
382       script = Forwarded(script);
383       e.rekeyFront(script);
384     }
385   }
386 }
387 
388 #ifdef JSGC_HASH_TABLE_CHECKS
checkStringsMapAfterMovingGC()389 void GeckoProfilerRuntime::checkStringsMapAfterMovingGC() {
390   for (auto r = strings().all(); !r.empty(); r.popFront()) {
391     BaseScript* script = r.front().key();
392     CheckGCThingAfterMovingGC(script);
393     auto ptr = strings().lookup(script);
394     MOZ_RELEASE_ASSERT(ptr.found() && &*ptr == &r.front());
395   }
396 }
397 #endif
398 
trace(JSTracer * trc)399 void ProfilingStackFrame::trace(JSTracer* trc) {
400   if (isJsFrame()) {
401     JSScript* s = rawScript();
402     TraceNullableRoot(trc, &s, "ProfilingStackFrame script");
403     spOrScript = s;
404   }
405 }
406 
GeckoProfilerBaselineOSRMarker(JSContext * cx,bool hasProfilerFrame MOZ_GUARD_OBJECT_NOTIFIER_PARAM_IN_IMPL)407 GeckoProfilerBaselineOSRMarker::GeckoProfilerBaselineOSRMarker(
408     JSContext* cx,
409     bool hasProfilerFrame MOZ_GUARD_OBJECT_NOTIFIER_PARAM_IN_IMPL)
410     : profiler(&cx->geckoProfiler()) {
411   MOZ_GUARD_OBJECT_NOTIFIER_INIT;
412   if (!hasProfilerFrame || !cx->runtime()->geckoProfiler().enabled()) {
413     profiler = nullptr;
414     return;
415   }
416 
417   uint32_t sp = profiler->profilingStack_->stackPointer;
418   if (sp >= profiler->profilingStack_->stackCapacity()) {
419     profiler = nullptr;
420     return;
421   }
422 
423   spBefore_ = sp;
424   if (sp == 0) {
425     return;
426   }
427 
428   ProfilingStackFrame& frame = profiler->profilingStack_->frames[sp - 1];
429   MOZ_ASSERT(!frame.isOSRFrame());
430   frame.setIsOSRFrame(true);
431 }
432 
~GeckoProfilerBaselineOSRMarker()433 GeckoProfilerBaselineOSRMarker::~GeckoProfilerBaselineOSRMarker() {
434   if (profiler == nullptr) {
435     return;
436   }
437 
438   uint32_t sp = profiler->stackPointer();
439   MOZ_ASSERT(spBefore_ == sp);
440   if (sp == 0) {
441     return;
442   }
443 
444   ProfilingStackFrame& frame = profiler->stack()[sp - 1];
445   MOZ_ASSERT(frame.isOSRFrame());
446   frame.setIsOSRFrame(false);
447 }
448 
script() const449 JS_PUBLIC_API JSScript* ProfilingStackFrame::script() const {
450   MOZ_ASSERT(isJsFrame());
451   auto script = reinterpret_cast<JSScript*>(spOrScript.operator void*());
452   if (!script) {
453     return nullptr;
454   }
455 
456   // If profiling is supressed then we can't trust the script pointers to be
457   // valid as they could be in the process of being moved by a compacting GC
458   // (although it's still OK to get the runtime from them).
459   JSContext* cx = script->runtimeFromAnyThread()->mainContextFromAnyThread();
460   if (!cx->isProfilerSamplingEnabled()) {
461     return nullptr;
462   }
463 
464   MOZ_ASSERT(!IsForwarded(script));
465   return script;
466 }
467 
pc() const468 JS_FRIEND_API jsbytecode* ProfilingStackFrame::pc() const {
469   MOZ_ASSERT(isJsFrame());
470   if (pcOffsetIfJS_ == NullPCOffset) {
471     return nullptr;
472   }
473 
474   JSScript* script = this->script();
475   return script ? script->offsetToPC(pcOffsetIfJS_) : nullptr;
476 }
477 
478 /* static */
pcToOffset(JSScript * aScript,jsbytecode * aPc)479 int32_t ProfilingStackFrame::pcToOffset(JSScript* aScript, jsbytecode* aPc) {
480   return aPc ? aScript->pcToOffset(aPc) : NullPCOffset;
481 }
482 
setPC(jsbytecode * pc)483 void ProfilingStackFrame::setPC(jsbytecode* pc) {
484   MOZ_ASSERT(isJsFrame());
485   JSScript* script = this->script();
486   MOZ_ASSERT(
487       script);  // This should not be called while profiling is suppressed.
488   pcOffsetIfJS_ = pcToOffset(script, pc);
489 }
490 
SetContextProfilingStack(JSContext * cx,ProfilingStack * profilingStack)491 JS_FRIEND_API void js::SetContextProfilingStack(
492     JSContext* cx, ProfilingStack* profilingStack) {
493   cx->geckoProfiler().setProfilingStack(
494       profilingStack, cx->runtime()->geckoProfiler().enabled());
495 }
496 
EnableContextProfilingStack(JSContext * cx,bool enabled)497 JS_FRIEND_API void js::EnableContextProfilingStack(JSContext* cx,
498                                                    bool enabled) {
499   cx->geckoProfiler().enable(enabled);
500   cx->runtime()->geckoProfiler().enable(enabled);
501 }
502 
RegisterContextProfilingEventMarker(JSContext * cx,void (* fn)(const char *))503 JS_FRIEND_API void js::RegisterContextProfilingEventMarker(
504     JSContext* cx, void (*fn)(const char*)) {
505   MOZ_ASSERT(cx->runtime()->geckoProfiler().enabled());
506   cx->runtime()->geckoProfiler().setEventMarker(fn);
507 }
508 
AutoSuppressProfilerSampling(JSContext * cx MOZ_GUARD_OBJECT_NOTIFIER_PARAM_IN_IMPL)509 AutoSuppressProfilerSampling::AutoSuppressProfilerSampling(
510     JSContext* cx MOZ_GUARD_OBJECT_NOTIFIER_PARAM_IN_IMPL)
511     : cx_(cx), previouslyEnabled_(cx->isProfilerSamplingEnabled()) {
512   MOZ_GUARD_OBJECT_NOTIFIER_INIT;
513   if (previouslyEnabled_) {
514     cx_->disableProfilerSampling();
515   }
516 }
517 
~AutoSuppressProfilerSampling()518 AutoSuppressProfilerSampling::~AutoSuppressProfilerSampling() {
519   if (previouslyEnabled_) {
520     cx_->enableProfilerSampling();
521   }
522 }
523 
524 namespace JS {
525 
526 // clang-format off
527 
528 // ProfilingSubcategory_X:
529 // One enum for each category X, listing that category's subcategories. This
530 // allows the sProfilingCategoryInfo macro construction below to look up a
531 // per-category index for a subcategory.
532 #define SUBCATEGORY_ENUMS_BEGIN_CATEGORY(name, labelAsString, color) \
533   enum class ProfilingSubcategory_##name : uint32_t {
534 #define SUBCATEGORY_ENUMS_SUBCATEGORY(category, name, labelAsString) \
535     name,
536 #define SUBCATEGORY_ENUMS_END_CATEGORY \
537   };
538 PROFILING_CATEGORY_LIST(SUBCATEGORY_ENUMS_BEGIN_CATEGORY,
539                         SUBCATEGORY_ENUMS_SUBCATEGORY,
540                         SUBCATEGORY_ENUMS_END_CATEGORY)
541 #undef SUBCATEGORY_ENUMS_BEGIN_CATEGORY
542 #undef SUBCATEGORY_ENUMS_SUBCATEGORY
543 #undef SUBCATEGORY_ENUMS_END_CATEGORY
544 
545 // sProfilingCategoryPairInfo:
546 // A list of ProfilingCategoryPairInfos with the same order as
547 // ProfilingCategoryPair, which can be used to map a ProfilingCategoryPair to
548 // its information.
549 #define CATEGORY_INFO_BEGIN_CATEGORY(name, labelAsString, color)
550 #define CATEGORY_INFO_SUBCATEGORY(category, name, labelAsString) \
551   {ProfilingCategory::category,                                  \
552    uint32_t(ProfilingSubcategory_##category::name), labelAsString},
553 #define CATEGORY_INFO_END_CATEGORY
554 const ProfilingCategoryPairInfo sProfilingCategoryPairInfo[] = {
555   PROFILING_CATEGORY_LIST(CATEGORY_INFO_BEGIN_CATEGORY,
556                           CATEGORY_INFO_SUBCATEGORY,
557                           CATEGORY_INFO_END_CATEGORY)
558 };
559 #undef CATEGORY_INFO_BEGIN_CATEGORY
560 #undef CATEGORY_INFO_SUBCATEGORY
561 #undef CATEGORY_INFO_END_CATEGORY
562 
563 // clang-format on
564 
GetProfilingCategoryPairInfo(ProfilingCategoryPair aCategoryPair)565 JS_FRIEND_API const ProfilingCategoryPairInfo& GetProfilingCategoryPairInfo(
566     ProfilingCategoryPair aCategoryPair) {
567   static_assert(
568       MOZ_ARRAY_LENGTH(sProfilingCategoryPairInfo) ==
569           uint32_t(ProfilingCategoryPair::COUNT),
570       "sProfilingCategoryPairInfo and ProfilingCategory need to have the "
571       "same order and the same length");
572 
573   uint32_t categoryPairIndex = uint32_t(aCategoryPair);
574   MOZ_RELEASE_ASSERT(categoryPairIndex <=
575                      uint32_t(ProfilingCategoryPair::LAST));
576   return sProfilingCategoryPairInfo[categoryPairIndex];
577 }
578 
579 }  // namespace JS
580