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