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