/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 4 -*- * vim: set ts=8 sts=4 et sw=4 tw=99: * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ #include "vm/SPSProfiler.h" #include "mozilla/DebugOnly.h" #include "jsnum.h" #include "jsprf.h" #include "jsscript.h" #include "jit/BaselineFrame.h" #include "jit/BaselineJIT.h" #include "jit/JitcodeMap.h" #include "jit/JitFrameIterator.h" #include "jit/JitFrames.h" #include "vm/StringBuffer.h" using namespace js; using mozilla::DebugOnly; SPSProfiler::SPSProfiler(JSRuntime* rt) : rt(rt), strings(mutexid::SPSProfilerStrings), stack_(nullptr), size_(nullptr), max_(0), slowAssertions(false), enabled_(false), eventMarker_(nullptr) { MOZ_ASSERT(rt != nullptr); } bool SPSProfiler::init() { auto locked = strings.lock(); if (!locked->init()) return false; return true; } void SPSProfiler::setProfilingStack(ProfileEntry* stack, uint32_t* size, uint32_t max) { MOZ_ASSERT_IF(size_ && *size_ != 0, !enabled()); MOZ_ASSERT(strings.lock()->initialized()); stack_ = stack; size_ = size; max_ = max; } void SPSProfiler::setEventMarker(void (*fn)(const char*)) { eventMarker_ = fn; } void SPSProfiler::enable(bool enabled) { MOZ_ASSERT(installed()); if (enabled_ == enabled) return; /* * Ensure all future generated code will be instrumented, or that all * currently instrumented code is discarded */ ReleaseAllJITCode(rt->defaultFreeOp()); // This function is called when the Gecko profiler makes a new TableTicker // (and thus, a new circular buffer). Set all current entries in the // JitcodeGlobalTable as expired and reset the buffer generation and lap // count. if (rt->hasJitRuntime() && rt->jitRuntime()->hasJitcodeGlobalTable()) rt->jitRuntime()->getJitcodeGlobalTable()->setAllEntriesAsExpired(rt); rt->resetProfilerSampleBufferGen(); rt->resetProfilerSampleBufferLapCount(); // Ensure that lastProfilingFrame is null before 'enabled' becomes true. if (rt->jitActivation) { rt->jitActivation->setLastProfilingFrame(nullptr); rt->jitActivation->setLastProfilingCallSite(nullptr); } enabled_ = enabled; /* Toggle SPS-related jumps on baseline jitcode. * The call to |ReleaseAllJITCode| above will release most baseline jitcode, but not * jitcode for scripts with active frames on the stack. These scripts need to have * their profiler state toggled so they behave properly. */ jit::ToggleBaselineProfiling(rt, enabled); /* Update lastProfilingFrame to point to the top-most JS jit-frame currently on * stack. */ if (rt->jitActivation) { // Walk through all activations, and set their lastProfilingFrame appropriately. if (enabled) { void* lastProfilingFrame = GetTopProfilingJitFrame(rt->jitTop); jit::JitActivation* jitActivation = rt->jitActivation; while (jitActivation) { jitActivation->setLastProfilingFrame(lastProfilingFrame); jitActivation->setLastProfilingCallSite(nullptr); lastProfilingFrame = GetTopProfilingJitFrame(jitActivation->prevJitTop()); jitActivation = jitActivation->prevJitActivation(); } } else { jit::JitActivation* jitActivation = rt->jitActivation; while (jitActivation) { jitActivation->setLastProfilingFrame(nullptr); jitActivation->setLastProfilingCallSite(nullptr); jitActivation = jitActivation->prevJitActivation(); } } } } /* Lookup the string for the function/script, creating one if necessary */ const char* SPSProfiler::profileString(JSScript* script, JSFunction* maybeFun) { auto locked = strings.lock(); MOZ_ASSERT(locked->initialized()); ProfileStringMap::AddPtr s = locked->lookupForAdd(script); if (!s) { auto str = allocProfileString(script, maybeFun); if (!str || !locked->add(s, script, mozilla::Move(str))) return nullptr; } return s->value().get(); } void SPSProfiler::onScriptFinalized(JSScript* script) { /* * This function is called whenever a script is destroyed, regardless of * whether profiling has been turned on, so don't invoke a function on an * invalid hash set. Also, even if profiling was enabled but then turned * off, we still want to remove the string, so no check of enabled() is * done. */ auto locked = strings.lock(); if (!locked->initialized()) return; if (ProfileStringMap::Ptr entry = locked->lookup(script)) locked->remove(entry); } void SPSProfiler::markEvent(const char* event) { MOZ_ASSERT(enabled()); if (eventMarker_) { JS::AutoSuppressGCAnalysis nogc; eventMarker_(event); } } bool SPSProfiler::enter(JSContext* cx, JSScript* script, JSFunction* maybeFun) { const char* str = profileString(script, maybeFun); if (str == nullptr) { ReportOutOfMemory(cx); return false; } #ifdef DEBUG // In debug builds, assert the JS pseudo frames already on the stack // have a non-null pc. Only look at the top frames to avoid quadratic // behavior. if (*size_ > 0 && *size_ - 1 < max_) { size_t start = (*size_ > 4) ? *size_ - 4 : 0; for (size_t i = start; i < *size_ - 1; i++) MOZ_ASSERT_IF(stack_[i].isJs(), stack_[i].pc() != nullptr); } #endif push(str, nullptr, script, script->code(), /* copy = */ true); return true; } void SPSProfiler::exit(JSScript* script, JSFunction* maybeFun) { pop(); #ifdef DEBUG /* Sanity check to make sure push/pop balanced */ if (*size_ < max_) { const char* str = profileString(script, maybeFun); /* Can't fail lookup because we should already be in the set */ MOZ_ASSERT(str != nullptr); // Bug 822041 if (!stack_[*size_].isJs()) { fprintf(stderr, "--- ABOUT TO FAIL ASSERTION ---\n"); fprintf(stderr, " stack=%p size=%d/%d\n", (void*) stack_, *size_, max_); for (int32_t i = *size_; i >= 0; i--) { if (stack_[i].isJs()) fprintf(stderr, " [%d] JS %s\n", i, stack_[i].label()); else fprintf(stderr, " [%d] C line %d %s\n", i, stack_[i].line(), stack_[i].label()); } } MOZ_ASSERT(stack_[*size_].isJs()); MOZ_ASSERT(stack_[*size_].script() == script); MOZ_ASSERT(strcmp((const char*) stack_[*size_].label(), str) == 0); stack_[*size_].setLabel(nullptr); stack_[*size_].setPC(nullptr); } #endif } void SPSProfiler::beginPseudoJS(const char* string, void* sp) { /* these operations cannot be re-ordered, so volatile-ize operations */ volatile ProfileEntry* stack = stack_; volatile uint32_t* size = size_; uint32_t current = *size; MOZ_ASSERT(installed()); if (current < max_) { stack[current].setLabel(string); stack[current].initCppFrame(sp, 0); stack[current].setFlag(ProfileEntry::BEGIN_PSEUDO_JS); } *size = current + 1; } void SPSProfiler::push(const char* string, void* sp, JSScript* script, jsbytecode* pc, bool copy, ProfileEntry::Category category) { MOZ_ASSERT_IF(sp != nullptr, script == nullptr && pc == nullptr); MOZ_ASSERT_IF(sp == nullptr, script != nullptr && pc != nullptr); /* these operations cannot be re-ordered, so volatile-ize operations */ volatile ProfileEntry* stack = stack_; volatile uint32_t* size = size_; uint32_t current = *size; MOZ_ASSERT(installed()); if (current < max_) { volatile ProfileEntry& entry = stack[current]; if (sp != nullptr) { entry.initCppFrame(sp, 0); MOZ_ASSERT(entry.flags() == js::ProfileEntry::IS_CPP_ENTRY); } else { entry.initJsFrame(script, pc); MOZ_ASSERT(entry.flags() == 0); } entry.setLabel(string); entry.setCategory(category); // Track if mLabel needs a copy. if (copy) entry.setFlag(js::ProfileEntry::FRAME_LABEL_COPY); else entry.unsetFlag(js::ProfileEntry::FRAME_LABEL_COPY); } *size = current + 1; } void SPSProfiler::pop() { MOZ_ASSERT(installed()); MOZ_ASSERT(*size_ > 0); (*size_)--; } /* * Serializes the script/function pair into a "descriptive string" which is * allowed to fail. This function cannot trigger a GC because it could finalize * some scripts, resize the hash table of profile strings, and invalidate the * AddPtr held while invoking allocProfileString. */ UniqueChars SPSProfiler::allocProfileString(JSScript* script, JSFunction* maybeFun) { // Note: this profiler string is regexp-matched by // devtools/client/profiler/cleopatra/js/parserWorker.js. // Get the function name, if any. JSAtom* atom = maybeFun ? maybeFun->displayAtom() : nullptr; // Get the script filename, if any, and its length. const char* filename = script->filename(); if (filename == nullptr) filename = ""; size_t lenFilename = strlen(filename); // Get the line number and its length as a string. uint64_t lineno = script->lineno(); size_t lenLineno = 1; for (uint64_t i = lineno; i /= 10; lenLineno++); // Determine the required buffer size. size_t len = lenFilename + lenLineno + 1; // +1 for the ":" separating them. if (atom) { len += JS::GetDeflatedUTF8StringLength(atom) + 3; // +3 for the " (" and ")" it adds. } // Allocate the buffer. UniqueChars cstr(js_pod_malloc(len + 1)); if (!cstr) return nullptr; // Construct the descriptive string. DebugOnly ret; if (atom) { UniqueChars atomStr = StringToNewUTF8CharsZ(nullptr, *atom); if (!atomStr) return nullptr; ret = snprintf(cstr.get(), len + 1, "%s (%s:%" PRIu64 ")", atomStr.get(), filename, lineno); } else { ret = snprintf(cstr.get(), len + 1, "%s:%" PRIu64, filename, lineno); } MOZ_ASSERT(ret == len, "Computed length should match actual length!"); return cstr; } void SPSProfiler::trace(JSTracer* trc) { if (stack_) { size_t limit = Min(*size_, max_); for (size_t i = 0; i < limit; i++) stack_[i].trace(trc); } } void SPSProfiler::fixupStringsMapAfterMovingGC() { auto locked = strings.lock(); if (!locked->initialized()) return; for (ProfileStringMap::Enum e(locked.get()); !e.empty(); e.popFront()) { JSScript* script = e.front().key(); if (IsForwarded(script)) { script = Forwarded(script); e.rekeyFront(script); } } } #ifdef JSGC_HASH_TABLE_CHECKS void SPSProfiler::checkStringsMapAfterMovingGC() { auto locked = strings.lock(); if (!locked->initialized()) return; for (auto r = locked->all(); !r.empty(); r.popFront()) { JSScript* script = r.front().key(); CheckGCThingAfterMovingGC(script); auto ptr = locked->lookup(script); MOZ_RELEASE_ASSERT(ptr.found() && &*ptr == &r.front()); } } #endif void ProfileEntry::trace(JSTracer* trc) { if (isJs()) { JSScript* s = rawScript(); TraceNullableRoot(trc, &s, "ProfileEntry script"); spOrScript = s; } } SPSEntryMarker::SPSEntryMarker(JSRuntime* rt, JSScript* script MOZ_GUARD_OBJECT_NOTIFIER_PARAM_IN_IMPL) : profiler(&rt->spsProfiler) { MOZ_GUARD_OBJECT_NOTIFIER_INIT; if (!profiler->installed()) { profiler = nullptr; return; } size_before = *profiler->size_; // We want to push a CPP frame so the profiler can correctly order JS and native stacks. profiler->beginPseudoJS("js::RunScript", this); profiler->push("js::RunScript", nullptr, script, script->code(), /* copy = */ false); } SPSEntryMarker::~SPSEntryMarker() { if (profiler == nullptr) return; profiler->pop(); profiler->endPseudoJS(); MOZ_ASSERT(size_before == *profiler->size_); } AutoSPSEntry::AutoSPSEntry(JSRuntime* rt, const char* label, ProfileEntry::Category category MOZ_GUARD_OBJECT_NOTIFIER_PARAM_IN_IMPL) : profiler_(&rt->spsProfiler) { MOZ_GUARD_OBJECT_NOTIFIER_INIT; if (!profiler_->installed()) { profiler_ = nullptr; return; } sizeBefore_ = *profiler_->size_; profiler_->beginPseudoJS(label, this); profiler_->push(label, this, nullptr, nullptr, /* copy = */ false, category); } AutoSPSEntry::~AutoSPSEntry() { if (!profiler_) return; profiler_->pop(); profiler_->endPseudoJS(); MOZ_ASSERT(sizeBefore_ == *profiler_->size_); } SPSBaselineOSRMarker::SPSBaselineOSRMarker(JSRuntime* rt, bool hasSPSFrame MOZ_GUARD_OBJECT_NOTIFIER_PARAM_IN_IMPL) : profiler(&rt->spsProfiler) { MOZ_GUARD_OBJECT_NOTIFIER_INIT; if (!hasSPSFrame || !profiler->enabled() || profiler->size() >= profiler->maxSize()) { profiler = nullptr; return; } size_before = profiler->size(); if (profiler->size() == 0) return; ProfileEntry& entry = profiler->stack()[profiler->size() - 1]; MOZ_ASSERT(entry.isJs()); entry.setOSR(); } SPSBaselineOSRMarker::~SPSBaselineOSRMarker() { if (profiler == nullptr) return; MOZ_ASSERT(size_before == *profiler->size_); if (profiler->size() == 0) return; ProfileEntry& entry = profiler->stack()[profiler->size() - 1]; MOZ_ASSERT(entry.isJs()); entry.unsetOSR(); } JS_PUBLIC_API(JSScript*) ProfileEntry::script() const volatile { MOZ_ASSERT(isJs()); auto script = reinterpret_cast(spOrScript); if (!script) return nullptr; // If profiling is supressed then we can't trust the script pointers to be // valid as they could be in the process of being moved by a compacting GC // (although it's still OK to get the runtime from them). JSRuntime* rt = script->zoneFromAnyThread()->runtimeFromAnyThread(); if (!rt->isProfilerSamplingEnabled()) return nullptr; MOZ_ASSERT(!IsForwarded(script)); return script; } JS_FRIEND_API(jsbytecode*) ProfileEntry::pc() const volatile { MOZ_ASSERT(isJs()); if (lineOrPcOffset == NullPCOffset) return nullptr; JSScript* script = this->script(); return script ? script->offsetToPC(lineOrPcOffset) : nullptr; } JS_FRIEND_API(void) ProfileEntry::setPC(jsbytecode* pc) volatile { MOZ_ASSERT(isJs()); JSScript* script = this->script(); MOZ_ASSERT(script); // This should not be called while profiling is suppressed. lineOrPcOffset = pc == nullptr ? NullPCOffset : script->pcToOffset(pc); } JS_FRIEND_API(void) js::SetContextProfilingStack(JSContext* cx, ProfileEntry* stack, uint32_t* size, uint32_t max) { cx->spsProfiler.setProfilingStack(stack, size, max); } JS_FRIEND_API(void) js::EnableContextProfilingStack(JSContext* cx, bool enabled) { cx->spsProfiler.enable(enabled); } JS_FRIEND_API(void) js::RegisterContextProfilingEventMarker(JSContext* cx, void (*fn)(const char*)) { MOZ_ASSERT(cx->spsProfiler.enabled()); cx->spsProfiler.setEventMarker(fn); } JS_FRIEND_API(jsbytecode*) js::ProfilingGetPC(JSContext* cx, JSScript* script, void* ip) { return cx->spsProfiler.ipToPC(script, size_t(ip)); } AutoSuppressProfilerSampling::AutoSuppressProfilerSampling(JSContext* cx MOZ_GUARD_OBJECT_NOTIFIER_PARAM_IN_IMPL) : rt_(cx->runtime()), previouslyEnabled_(rt_->isProfilerSamplingEnabled()) { MOZ_GUARD_OBJECT_NOTIFIER_INIT; if (previouslyEnabled_) rt_->disableProfilerSampling(); } AutoSuppressProfilerSampling::AutoSuppressProfilerSampling(JSRuntime* rt MOZ_GUARD_OBJECT_NOTIFIER_PARAM_IN_IMPL) : rt_(rt), previouslyEnabled_(rt_->isProfilerSamplingEnabled()) { MOZ_GUARD_OBJECT_NOTIFIER_INIT; if (previouslyEnabled_) rt_->disableProfilerSampling(); } AutoSuppressProfilerSampling::~AutoSuppressProfilerSampling() { if (previouslyEnabled_) rt_->enableProfilerSampling(); } void* js::GetTopProfilingJitFrame(uint8_t* exitFramePtr) { // For null exitFrame, there is no previous exit frame, just return. if (!exitFramePtr) return nullptr; jit::JitProfilingFrameIterator iter(exitFramePtr); MOZ_ASSERT(!iter.done()); return iter.fp(); }