Bug 1367406 - Part 1: Add interleaved stack functionality to HangStack, r=froydnj

- This patch was written in 3 interdependent parts, which are described below -

Part 1A: Allow HangStack to contain raw PCs and Module offsets, r=froydnj

The HangStack previously consisted of an array of const char* pointers into its
backing string buffer, which represented pseudostack entries. With interleaved
stacks, it is now possible for the stack to contain raw unresolved program
counters (Kind::PC), and module/offset pairs (Kind::MODOFFSET). To do this, we
use a discriminated union, and make the backing array use the discriminated
union instead of const char*s.

The code cannot use mozilla::Variant<const char*, uintptr_t, Module>
unfortuantely, as we cannot use the implementation of ParamTraits for Variant in
HangStack's ParamTraits implementation.

When deserializing a HangStack over IPC, we need to read the string frame
entries into the backing string buffer, and generate const char* entries for
each of the strings which we read in over IPC. The default implementation of
ParamTraits wouldn't give us access to the enclusing HangStack object while
deserializing each individual entry, so we couldn't use it. In fact, Entries
don't have ParamTraits implemented for them at all, and can only be sent over
IPC as part of a HangStack due to this dependency.

Part 1B: Remove nsIHangDetails.pseudoStack, replace ProcessedStack w/ new HangStack type, r=froydnj

Previously there were two stack objects on each HangDetails object: mStack and
mPseudoStack. mStack was a Telemetry::ProcessedStack, while mPseudoStack was a
HangStack. After the changes in part 1A, HangStack can now contain all of the
information of both the old HangStack and ProcessedStack, so the mPseudoStack
field is renamed to mStack, and the old mStack field is removed.

This patch also implements the new GetStack getter, which generates the JS data
format for the new HangStack type.

Part 1C: Collect interleaved stacks w/ ProfilerStackCollector API in ThreadStackHelper, r=froydnj

This new API was added by njn in bug 1380286, and provides both pseudostack and
native stack entries to the consumer of the API.

This patch changes ThreadStackHelper to use this new API instead of the previous
one, and use it to collect the frames directly into HangStack objects.
This commit is contained in:
Michael Layzell
2017-08-10 16:40:59 -04:00
parent d1ec419fb4
commit 9782242b7e
10 changed files with 524 additions and 518 deletions

View File

@@ -22,6 +22,7 @@
#include "mozilla/MemoryChecking.h"
#include "mozilla/Sprintf.h"
#include "nsThread.h"
#include "mozilla/HangStack.h"
#ifdef __GNUC__
# pragma GCC diagnostic push
@@ -66,19 +67,43 @@
namespace mozilla {
ThreadStackHelper::ThreadStackHelper()
#ifdef MOZ_THREADSTACKHELPER_PSEUDO
: mStackToFill(nullptr)
, mPseudoStack(profiler_get_pseudo_stack())
, mMaxStackSize(Stack::sMaxInlineStorage)
, mMaxStackSize(HangStack::sMaxInlineStorage)
, mMaxBufferSize(512)
#endif
#ifdef MOZ_THREADSTACKHELPER_NATIVE
, mNativeStackToFill(nullptr)
#endif
, mDesiredStackSize(0)
, mDesiredBufferSize(0)
{
mThreadId = profiler_current_thread_id();
}
bool
ThreadStackHelper::PrepareStackBuffer(HangStack& aStack)
{
// If we need to grow because we used more than we could store last time,
// increase our maximum sizes for this time.
if (mDesiredBufferSize > mMaxBufferSize) {
mMaxBufferSize = mDesiredBufferSize;
}
if (mDesiredStackSize > mMaxStackSize) {
mMaxStackSize = mDesiredStackSize;
}
mDesiredBufferSize = 0;
mDesiredStackSize = 0;
// Return false to skip getting the stack and return an empty stack
aStack.clear();
#ifdef MOZ_THREADSTACKHELPER_PSEUDO
if (!aStack.reserve(mMaxStackSize) ||
!aStack.reserve(aStack.capacity()) || // reserve up to the capacity
!aStack.EnsureBufferCapacity(mMaxBufferSize)) {
return false;
}
return true;
#else
return false;
#endif
}
namespace {
template<typename T>
class ScopedSetPtr
@@ -92,116 +117,86 @@ public:
} // namespace
void
ThreadStackHelper::GetPseudoStack(Stack& aStack, nsACString& aRunnableName)
{
GetStacksInternal(&aStack, nullptr, aRunnableName);
}
void
ThreadStackHelper::GetNativeStack(NativeStack& aNativeStack, nsACString& aRunnableName)
{
GetStacksInternal(nullptr, &aNativeStack, aRunnableName);
}
void
ThreadStackHelper::GetPseudoAndNativeStack(Stack& aStack,
NativeStack& aNativeStack,
nsACString& aRunnableName)
{
GetStacksInternal(&aStack, &aNativeStack, aRunnableName);
}
void
ThreadStackHelper::GetStacksInternal(Stack* aStack,
NativeStack* aNativeStack,
nsACString& aRunnableName)
ThreadStackHelper::GetStack(HangStack& aStack, nsACString& aRunnableName, bool aStackWalk)
{
aRunnableName.AssignLiteral("???");
#if defined(MOZ_THREADSTACKHELPER_PSEUDO) || defined(MOZ_THREADSTACKHELPER_NATIVE)
// Always run PrepareStackBuffer first to clear aStack
if (aStack && !PrepareStackBuffer(*aStack)) {
// Skip and return empty aStack
if (!PrepareStackBuffer(aStack)) {
return;
}
// Prepare the native stack
if (aNativeStack) {
aNativeStack->clear();
aNativeStack->reserve(HangStack::sMaxNativeFrames);
}
#ifdef MOZ_THREADSTACKHELPER_PSEUDO
ScopedSetPtr<Stack> stackPtr(mStackToFill, aStack);
#endif
#ifdef MOZ_THREADSTACKHELPER_NATIVE
ScopedSetPtr<NativeStack> nativeStackPtr(mNativeStackToFill, aNativeStack);
#endif
Array<char, nsThread::kRunnableNameBufSize> runnableName;
runnableName[0] = '\0';
auto callback = [&, this] (void** aPCs, size_t aCount, bool aIsMainThread) {
// NOTE: We cannot allocate any memory in this callback, as the target
// thread is suspended, so we first copy it into a stack-allocated buffer,
// and then once the target thread is resumed, we can copy it into a real
// nsCString.
//
// Currently we only store the names of runnables which are running on the
// main thread, so we only want to read sMainThreadRunnableName and copy its
// value in the case that we are currently suspending the main thread.
if (aIsMainThread) {
runnableName = nsThread::sMainThreadRunnableName;
}
#ifdef MOZ_THREADSTACKHELPER_PSEUDO
if (mStackToFill) {
FillStackBuffer();
}
#endif
ScopedSetPtr<HangStack> _stackGuard(mStackToFill, &aStack);
ScopedSetPtr<Array<char, nsThread::kRunnableNameBufSize>>
_runnableGuard(mRunnableNameBuffer, &runnableName);
#ifdef MOZ_THREADSTACKHELPER_NATIVE
if (mNativeStackToFill) {
while (aCount-- &&
mNativeStackToFill->size() < mNativeStackToFill->capacity()) {
mNativeStackToFill->push_back(reinterpret_cast<uintptr_t>(aPCs[aCount]));
}
}
#endif
};
if (mStackToFill || mNativeStackToFill) {
profiler_suspend_and_sample_thread(mThreadId,
callback,
/* aSampleNative = */ !!aNativeStack);
}
// XXX: We don't need to pass in ProfilerFeature::StackWalk to trigger
// stackwalking, as that is instead controlled by the last argument.
profiler_suspend_and_sample_thread(
mThreadId, ProfilerFeature::Privacy, *this, aStackWalk);
// Copy the name buffer allocation into the output string. We explicitly set
// the last byte to null in case we read in some corrupted data without a null
// terminator.
runnableName[nsThread::kRunnableNameBufSize - 1] = '\0';
aRunnableName.AssignASCII(runnableName.cbegin());
#endif
}
bool
ThreadStackHelper::PrepareStackBuffer(Stack& aStack)
void
ThreadStackHelper::SetIsMainThread()
{
// Return false to skip getting the stack and return an empty stack
aStack.clear();
#ifdef MOZ_THREADSTACKHELPER_PSEUDO
MOZ_ASSERT(mPseudoStack);
if (!aStack.reserve(mMaxStackSize) ||
!aStack.reserve(aStack.capacity()) || // reserve up to the capacity
!aStack.EnsureBufferCapacity(mMaxBufferSize)) {
return false;
}
return true;
#else
return false;
#endif
MOZ_RELEASE_ASSERT(mRunnableNameBuffer);
// NOTE: We cannot allocate any memory in this callback, as the target
// thread is suspended, so we first copy it into a stack-allocated buffer,
// and then once the target thread is resumed, we can copy it into a real
// nsCString.
//
// Currently we only store the names of runnables which are running on the
// main thread, so we only want to read sMainThreadRunnableName and copy its
// value in the case that we are currently suspending the main thread.
*mRunnableNameBuffer = nsThread::sMainThreadRunnableName;
}
#ifdef MOZ_THREADSTACKHELPER_PSEUDO
void
ThreadStackHelper::TryAppendFrame(HangStack::Frame aFrame)
{
MOZ_RELEASE_ASSERT(mStackToFill);
// Record that we _want_ to use another frame entry. If this exceeds
// mMaxStackSize, we'll allocate more room on the next hang.
mDesiredStackSize += 1;
// Perform the append if we have enough space to do so.
if (mStackToFill->canAppendWithoutRealloc(1)) {
mStackToFill->infallibleAppend(aFrame);
}
}
void
ThreadStackHelper::CollectNativeLeafAddr(void* aAddr)
{
MOZ_RELEASE_ASSERT(mStackToFill);
TryAppendFrame(HangStack::Frame(reinterpret_cast<uintptr_t>(aAddr)));
}
void
ThreadStackHelper::CollectJitReturnAddr(void* aAddr)
{
MOZ_RELEASE_ASSERT(mStackToFill);
TryAppendFrame(HangStack::Frame("(jit frame)"));
}
void
ThreadStackHelper::CollectWasmFrame(const char* aLabel)
{
MOZ_RELEASE_ASSERT(mStackToFill);
// We don't want to collect WASM frames, as they are probably for content, so
// we just add a "(content wasm)" frame.
TryAppendFrame(HangStack::Frame("(wasm)"));
}
namespace {
@@ -210,7 +205,6 @@ IsChromeJSScript(JSScript* aScript)
{
// May be called from another thread or inside a signal handler.
// We assume querying the script is safe but we must not manipulate it.
nsIScriptSecurityManager* const secman =
nsScriptSecurityManager::GetScriptSecurityManager();
NS_ENSURE_TRUE(secman, false);
@@ -250,122 +244,76 @@ GetPathAfterComponent(const char* filename, const char (&component)[LEN]) {
} // namespace
const char*
ThreadStackHelper::AppendJSEntry(const js::ProfileEntry* aEntry,
intptr_t& aAvailableBufferSize,
const char* aPrevLabel)
{
// May be called from another thread or inside a signal handler.
// We assume querying the script is safe but we must not manupulate it.
// Also we must not allocate any memory from heap.
MOZ_ASSERT(aEntry->isJs());
const char* label;
JSScript* script = aEntry->script();
if (!script) {
label = "(profiling suppressed)";
} else if (IsChromeJSScript(aEntry->script())) {
const char* filename = JS_GetScriptFilename(aEntry->script());
const unsigned lineno = JS_PCToLineNumber(aEntry->script(), aEntry->pc());
MOZ_ASSERT(filename);
char buffer[128]; // Enough to fit longest js file name from the tree
// Some script names are in the form "foo -> bar -> baz".
// Here we find the origin of these redirected scripts.
const char* basename = GetPathAfterComponent(filename, " -> ");
if (basename) {
filename = basename;
}
basename = GetFullPathForScheme(filename, "chrome://");
if (!basename) {
basename = GetFullPathForScheme(filename, "resource://");
}
if (!basename) {
// If the (add-on) script is located under the {profile}/extensions
// directory, extract the path after the /extensions/ part.
basename = GetPathAfterComponent(filename, "/extensions/");
}
if (!basename) {
// Only keep the file base name for paths outside the above formats.
basename = strrchr(filename, '/');
basename = basename ? basename + 1 : filename;
// Look for Windows path separator as well.
filename = strrchr(basename, '\\');
if (filename) {
basename = filename + 1;
}
}
size_t len = SprintfLiteral(buffer, "%s:%u", basename, lineno);
if (len < sizeof(buffer)) {
if (mStackToFill->IsSameAsEntry(aPrevLabel, buffer)) {
return aPrevLabel;
}
// Keep track of the required buffer size
aAvailableBufferSize -= (len + 1);
if (aAvailableBufferSize >= 0) {
// Buffer is big enough.
return mStackToFill->InfallibleAppendViaBuffer(buffer, len);
}
// Buffer is not big enough; fall through to using static label below.
}
// snprintf failed or buffer is not big enough.
label = "(chrome script)";
} else {
label = "(content script)";
}
if (mStackToFill->IsSameAsEntry(aPrevLabel, label)) {
return aPrevLabel;
}
mStackToFill->infallibleAppend(label);
return label;
}
#endif // MOZ_THREADSTACKHELPER_PSEUDO
void
ThreadStackHelper::FillStackBuffer()
ThreadStackHelper::CollectPseudoEntry(const js::ProfileEntry& aEntry)
{
#ifdef MOZ_THREADSTACKHELPER_PSEUDO
MOZ_ASSERT(mStackToFill->empty());
size_t reservedSize = mStackToFill->capacity();
size_t reservedBufferSize = mStackToFill->AvailableBufferSize();
intptr_t availableBufferSize = intptr_t(reservedBufferSize);
// Go from front to back
const js::ProfileEntry* entry = mPseudoStack->entries;
const js::ProfileEntry* end = entry + mPseudoStack->stackSize();
// Deduplicate identical, consecutive frames
const char* prevLabel = nullptr;
for (; reservedSize-- && entry != end; entry++) {
if (entry->isJs()) {
prevLabel = AppendJSEntry(entry, availableBufferSize, prevLabel);
continue;
}
const char* const label = entry->label();
if (mStackToFill->IsSameAsEntry(prevLabel, label)) {
// Avoid duplicate labels to save space in the stack.
continue;
}
mStackToFill->infallibleAppend(label);
prevLabel = label;
// For non-js frames we just include the raw label.
if (!aEntry.isJs()) {
const char* label = aEntry.label();
TryAppendFrame(HangStack::Frame(label));
return;
}
// end != entry if we exited early due to not enough reserved frames.
// Expand the number of reserved frames for next time.
mMaxStackSize = mStackToFill->capacity() + (end - entry);
// availableBufferSize < 0 if we needed a larger buffer than we reserved.
// Calculate a new reserve size for next time.
if (availableBufferSize < 0) {
mMaxBufferSize = reservedBufferSize - availableBufferSize;
if (!aEntry.script()) {
TryAppendFrame(HangStack::Frame("(profiling suppressed)"));
return;
}
if (!IsChromeJSScript(aEntry.script())) {
TryAppendFrame(HangStack::Frame("(content script)"));
return;
}
// Rather than using the profiler's dynamic string, we compute our own string.
// This is because we want to do some size-saving strategies, and throw out
// information which won't help us as much.
// XXX: We currently don't collect the function name which hung.
const char* filename = JS_GetScriptFilename(aEntry.script());
unsigned lineno = JS_PCToLineNumber(aEntry.script(), aEntry.pc());
// Some script names are in the form "foo -> bar -> baz".
// Here we find the origin of these redirected scripts.
const char* basename = GetPathAfterComponent(filename, " -> ");
if (basename) {
filename = basename;
}
// Strip chrome:// or resource:// off of the filename if present.
basename = GetFullPathForScheme(filename, "chrome://");
if (!basename) {
basename = GetFullPathForScheme(filename, "resource://");
}
if (!basename) {
// If we're in an add-on script, under the {profile}/extensions
// directory, extract the path after the /extensions/ part.
basename = GetPathAfterComponent(filename, "/extensions/");
}
if (!basename) {
// Only keep the file base name for paths outside the above formats.
basename = strrchr(filename, '/');
basename = basename ? basename + 1 : filename;
// Look for Windows path separator as well.
filename = strrchr(basename, '\\');
if (filename) {
basename = filename + 1;
}
}
mDesiredStackSize += 1;
char buffer[128]; // Enough to fit longest js file name from the tree
size_t len = SprintfLiteral(buffer, "%s:%u", basename, lineno);
if (len < sizeof(buffer)) {
mDesiredBufferSize += len + 1;
if (mStackToFill->canAppendWithoutRealloc(1) &&
mStackToFill->AvailableBufferSize() >= len + 1) {
mStackToFill->InfallibleAppendViaBuffer(buffer, len);
return;
}
}
if (mStackToFill->canAppendWithoutRealloc(1)) {
mStackToFill->infallibleAppend(HangStack::Frame("(chrome script)"));
}
#endif
}
} // namespace mozilla