/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2; -*- */ /* 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 "mozilla/HangAnnotations.h" #include "ThreadHangStats.h" #include "nsITelemetry.h" #include "HangReports.h" #include "jsapi.h" namespace { using namespace mozilla; using namespace mozilla::HangMonitor; using namespace mozilla::Telemetry; static JSObject* CreateJSTimeHistogram(JSContext* cx, const Telemetry::TimeHistogram& time) { /* Create JS representation of TimeHistogram, in the format of Chromium-style histograms. */ JS::RootedObject ret(cx, JS_NewPlainObject(cx)); if (!ret) { return nullptr; } if (!JS_DefineProperty(cx, ret, "min", time.GetBucketMin(0), JSPROP_ENUMERATE) || !JS_DefineProperty(cx, ret, "max", time.GetBucketMax(ArrayLength(time) - 1), JSPROP_ENUMERATE) || !JS_DefineProperty(cx, ret, "histogram_type", nsITelemetry::HISTOGRAM_EXPONENTIAL, JSPROP_ENUMERATE)) { return nullptr; } // TODO: calculate "sum" if (!JS_DefineProperty(cx, ret, "sum", 0, JSPROP_ENUMERATE)) { return nullptr; } JS::RootedObject ranges( cx, JS_NewArrayObject(cx, ArrayLength(time) + 1)); JS::RootedObject counts( cx, JS_NewArrayObject(cx, ArrayLength(time) + 1)); if (!ranges || !counts) { return nullptr; } /* In a Chromium-style histogram, the first bucket is an "under" bucket that represents all values below the histogram's range. */ if (!JS_DefineElement(cx, ranges, 0, time.GetBucketMin(0), JSPROP_ENUMERATE) || !JS_DefineElement(cx, counts, 0, 0, JSPROP_ENUMERATE)) { return nullptr; } for (size_t i = 0; i < ArrayLength(time); i++) { if (!JS_DefineElement(cx, ranges, i + 1, time.GetBucketMax(i), JSPROP_ENUMERATE) || !JS_DefineElement(cx, counts, i + 1, time[i], JSPROP_ENUMERATE)) { return nullptr; } } if (!JS_DefineProperty(cx, ret, "ranges", ranges, JSPROP_ENUMERATE) || !JS_DefineProperty(cx, ret, "counts", counts, JSPROP_ENUMERATE)) { return nullptr; } return ret; } static JSObject* CreateJSHangStack(JSContext* cx, const Telemetry::HangStack& stack) { JS::RootedObject ret(cx, JS_NewArrayObject(cx, stack.length())); if (!ret) { return nullptr; } for (size_t i = 0; i < stack.length(); i++) { JS::RootedString string(cx, JS_NewStringCopyZ(cx, stack[i])); if (!JS_DefineElement(cx, ret, i, string, JSPROP_ENUMERATE)) { return nullptr; } } return ret; } static void CreateJSHangAnnotations(JSContext* cx, const HangAnnotationsVector& annotations, JS::MutableHandleObject returnedObject) { JS::RootedObject annotationsArray(cx, JS_NewArrayObject(cx, 0)); if (!annotationsArray) { returnedObject.set(nullptr); return; } // We keep track of the annotations we reported in this hash set, so we can // discard duplicated ones. nsTHashtable reportedAnnotations; size_t annotationIndex = 0; for (const auto & curAnnotations : annotations) { JS::RootedObject jsAnnotation(cx, JS_NewPlainObject(cx)); if (!jsAnnotation) { continue; } // Build a key to index the current annotations in our hash set. nsAutoString annotationsKey; nsresult rv = ComputeAnnotationsKey(curAnnotations, annotationsKey); if (NS_FAILED(rv)) { continue; } // Check if the annotations are in the set. If that's the case, don't double report. if (reportedAnnotations.GetEntry(annotationsKey)) { continue; } // If not, report them. reportedAnnotations.PutEntry(annotationsKey); UniquePtr annotationsEnum = curAnnotations->GetEnumerator(); if (!annotationsEnum) { continue; } nsAutoString key; nsAutoString value; while (annotationsEnum->Next(key, value)) { JS::RootedValue jsValue(cx); jsValue.setString(JS_NewUCStringCopyN(cx, value.get(), value.Length())); if (!JS_DefineUCProperty(cx, jsAnnotation, key.get(), key.Length(), jsValue, JSPROP_ENUMERATE)) { returnedObject.set(nullptr); return; } } if (!JS_SetElement(cx, annotationsArray, annotationIndex, jsAnnotation)) { continue; } ++annotationIndex; } // Return the array using a |MutableHandleObject| to avoid triggering a false // positive rooting issue in the hazard analysis build. returnedObject.set(annotationsArray); } static JSObject* CreateJSHangHistogram(JSContext* cx, const Telemetry::HangHistogram& hang) { JS::RootedObject ret(cx, JS_NewPlainObject(cx)); if (!ret) { return nullptr; } JS::RootedObject stack(cx, CreateJSHangStack(cx, hang.GetStack())); JS::RootedObject time(cx, CreateJSTimeHistogram(cx, hang)); auto& hangAnnotations = hang.GetAnnotations(); JS::RootedObject annotations(cx); CreateJSHangAnnotations(cx, hangAnnotations, &annotations); if (!stack || !time || !annotations || !JS_DefineProperty(cx, ret, "stack", stack, JSPROP_ENUMERATE) || !JS_DefineProperty(cx, ret, "histogram", time, JSPROP_ENUMERATE) || (!hangAnnotations.empty() && // <-- Only define annotations when nonempty !JS_DefineProperty(cx, ret, "annotations", annotations, JSPROP_ENUMERATE))) { return nullptr; } return ret; } } // namespace namespace mozilla { namespace Telemetry { JSObject* CreateJSThreadHangStats(JSContext* cx, const Telemetry::ThreadHangStats& thread) { JS::RootedObject ret(cx, JS_NewPlainObject(cx)); if (!ret) { return nullptr; } JS::RootedString name(cx, JS_NewStringCopyZ(cx, thread.GetName())); if (!name || !JS_DefineProperty(cx, ret, "name", name, JSPROP_ENUMERATE)) { return nullptr; } JS::RootedObject activity(cx, CreateJSTimeHistogram(cx, thread.mActivity)); if (!activity || !JS_DefineProperty(cx, ret, "activity", activity, JSPROP_ENUMERATE)) { return nullptr; } // Process the hangs into a hangs object. JS::RootedObject hangs(cx, JS_NewArrayObject(cx, 0)); if (!hangs) { return nullptr; } for (size_t i = 0; i < thread.mHangs.length(); i++) { JS::RootedObject obj(cx, CreateJSHangHistogram(cx, thread.mHangs[i])); if (!ret) { return nullptr; } JS::RootedString runnableName(cx, JS_NewStringCopyZ(cx, thread.mHangs[i].GetRunnableName())); if (!runnableName || !JS_DefineProperty(cx, ret, "runnableName", runnableName, JSPROP_ENUMERATE)) { return nullptr; } // Check if we have a cached native stack index, and if we do record it. uint32_t index = thread.mHangs[i].GetNativeStackIndex(); if (index != Telemetry::HangHistogram::NO_NATIVE_STACK_INDEX) { if (!JS_DefineProperty(cx, obj, "nativeStack", index, JSPROP_ENUMERATE)) { return nullptr; } } if (!JS_DefineElement(cx, hangs, i, obj, JSPROP_ENUMERATE)) { return nullptr; } } if (!JS_DefineProperty(cx, ret, "hangs", hangs, JSPROP_ENUMERATE)) { return nullptr; } // We should already have a CombinedStacks object on the ThreadHangStats, so // add that one. JS::RootedObject fullReportObj(cx, CreateJSStackObject(cx, thread.mCombinedStacks)); if (!fullReportObj) { return nullptr; } if (!JS_DefineProperty(cx, ret, "nativeStacks", fullReportObj, JSPROP_ENUMERATE)) { return nullptr; } return ret; } void TimeHistogram::Add(PRIntervalTime aTime) { uint32_t timeMs = PR_IntervalToMilliseconds(aTime); size_t index = mozilla::FloorLog2(timeMs); operator[](index)++; } const char* HangStack::InfallibleAppendViaBuffer(const char* aText, size_t aLength) { MOZ_ASSERT(this->canAppendWithoutRealloc(1)); // Include null-terminator in length count. MOZ_ASSERT(mBuffer.canAppendWithoutRealloc(aLength + 1)); const char* const entry = mBuffer.end(); mBuffer.infallibleAppend(aText, aLength); mBuffer.infallibleAppend('\0'); // Explicitly append null-terminator this->infallibleAppend(entry); return entry; } const char* HangStack::AppendViaBuffer(const char* aText, size_t aLength) { if (!this->reserve(this->length() + 1)) { return nullptr; } // Keep track of the previous buffer in case we need to adjust pointers later. const char* const prevStart = mBuffer.begin(); const char* const prevEnd = mBuffer.end(); // Include null-terminator in length count. if (!mBuffer.reserve(mBuffer.length() + aLength + 1)) { return nullptr; } if (prevStart != mBuffer.begin()) { // The buffer has moved; we have to adjust pointers in the stack. for (auto & entry : *this) { if (entry >= prevStart && entry < prevEnd) { // Move from old buffer to new buffer. entry += mBuffer.begin() - prevStart; } } } return InfallibleAppendViaBuffer(aText, aLength); } uint32_t HangHistogram::GetHash(const HangStack& aStack) { uint32_t hash = 0; for (const char* const* label = aStack.begin(); label != aStack.end(); label++) { /* If the string is within our buffer, we need to hash its content. Otherwise, the string is statically allocated, and we only need to hash the pointer instead of the content. */ if (aStack.IsInBuffer(*label)) { hash = AddToHash(hash, HashString(*label)); } else { hash = AddToHash(hash, *label); } } return hash; } bool HangHistogram::operator==(const HangHistogram& aOther) const { if (mHash != aOther.mHash) { return false; } if (mStack.length() != aOther.mStack.length()) { return false; } return mStack == aOther.mStack; } } // namespace Telemetry } // namespace mozilla