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