diff options
Diffstat (limited to 'toolkit/components/telemetry/other')
12 files changed, 2175 insertions, 0 deletions
diff --git a/toolkit/components/telemetry/other/CombinedStacks.cpp b/toolkit/components/telemetry/other/CombinedStacks.cpp new file mode 100644 index 0000000000..61248f9b40 --- /dev/null +++ b/toolkit/components/telemetry/other/CombinedStacks.cpp @@ -0,0 +1,257 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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 "CombinedStacks.h" + +#include "jsapi.h" +#include "js/Array.h" // JS::NewArrayObject +#include "js/PropertyAndElement.h" // JS_DefineElement, JS_DefineProperty +#include "js/String.h" + +namespace mozilla::Telemetry { + +// The maximum number of chrome hangs stacks that we're keeping. +const size_t kMaxChromeStacksKept = 50; + +CombinedStacks::CombinedStacks() : CombinedStacks(kMaxChromeStacksKept) {} + +CombinedStacks::CombinedStacks(size_t aMaxStacksCount) + : mNextIndex(0), mMaxStacksCount(aMaxStacksCount) {} + +size_t CombinedStacks::GetMaxStacksCount() const { return mMaxStacksCount; } +size_t CombinedStacks::GetModuleCount() const { return mModules.size(); } + +const Telemetry::ProcessedStack::Module& CombinedStacks::GetModule( + unsigned aIndex) const { + return mModules[aIndex]; +} + +void CombinedStacks::AddFrame( + size_t aStackIndex, const ProcessedStack::Frame& aFrame, + const std::function<const ProcessedStack::Module&(int)>& aModuleGetter) { + uint16_t modIndex; + if (aFrame.mModIndex == std::numeric_limits<uint16_t>::max()) { + modIndex = aFrame.mModIndex; + } else { + const ProcessedStack::Module& module = aModuleGetter(aFrame.mModIndex); + auto modIterator = std::find(mModules.begin(), mModules.end(), module); + if (modIterator == mModules.end()) { + mModules.push_back(module); + modIndex = mModules.size() - 1; + } else { + modIndex = modIterator - mModules.begin(); + } + } + mStacks[aStackIndex].push_back( + ProcessedStack::Frame{aFrame.mOffset, modIndex}); +} + +size_t CombinedStacks::AddStack(const Telemetry::ProcessedStack& aStack) { + size_t index = mNextIndex; + // Advance the indices of the circular queue holding the stacks. + mNextIndex = (mNextIndex + 1) % mMaxStacksCount; + // Grow the vector up to the maximum size, if needed. + if (mStacks.size() < mMaxStacksCount) { + mStacks.resize(mStacks.size() + 1); + } + + // Clear the old stack before set. + mStacks[index].clear(); + + size_t stackSize = aStack.GetStackSize(); + for (size_t i = 0; i < stackSize; ++i) { + // Need to specify a return type in the following lambda, + // otherwise it's incorrectly deduced to be a non-reference type. + AddFrame(index, aStack.GetFrame(i), + [&aStack](int aIdx) -> const ProcessedStack::Module& { + return aStack.GetModule(aIdx); + }); + } + return index; +} + +void CombinedStacks::AddStacks(const CombinedStacks& aStacks) { + mStacks.resize( + std::min(mStacks.size() + aStacks.GetStackCount(), mMaxStacksCount)); + + for (const auto& stack : aStacks.mStacks) { + size_t index = mNextIndex; + // Advance the indices of the circular queue holding the stacks. + mNextIndex = (mNextIndex + 1) % mMaxStacksCount; + + // Clear the old stack before set. + mStacks[index].clear(); + + for (const auto& frame : stack) { + // Need to specify a return type in the following lambda, + // otherwise it's incorrectly deduced to be a non-reference type. + AddFrame(index, frame, + [&aStacks](int aIdx) -> const ProcessedStack::Module& { + return aStacks.mModules[aIdx]; + }); + } + } +} + +const CombinedStacks::Stack& CombinedStacks::GetStack(unsigned aIndex) const { + return mStacks[aIndex]; +} + +size_t CombinedStacks::GetStackCount() const { return mStacks.size(); } + +size_t CombinedStacks::SizeOfExcludingThis() const { + // This is a crude approximation. We would like to do something like + // aMallocSizeOf(&mModules[0]), but on linux aMallocSizeOf will call + // malloc_usable_size which is only safe on the pointers returned by malloc. + // While it works on current libstdc++, it is better to be safe and not assume + // that &vec[0] points to one. We could use a custom allocator, but + // it doesn't seem worth it. + size_t n = 0; + n += mModules.capacity() * sizeof(Telemetry::ProcessedStack::Module); + n += mStacks.capacity() * sizeof(Stack); + for (const auto& s : mStacks) { + n += s.capacity() * sizeof(Telemetry::ProcessedStack::Frame); + } + return n; +} + +void CombinedStacks::RemoveStack(unsigned aIndex) { + MOZ_ASSERT(aIndex < mStacks.size()); + + mStacks.erase(mStacks.begin() + aIndex); + + if (aIndex < mNextIndex) { + if (mNextIndex == 0) { + mNextIndex = mStacks.size(); + } else { + mNextIndex--; + } + } + + if (mNextIndex > mStacks.size()) { + mNextIndex = mStacks.size(); + } +} + +void CombinedStacks::Swap(CombinedStacks& aOther) { + mModules.swap(aOther.mModules); + mStacks.swap(aOther.mStacks); + + size_t nextIndex = aOther.mNextIndex; + aOther.mNextIndex = mNextIndex; + mNextIndex = nextIndex; + + size_t maxStacksCount = aOther.mMaxStacksCount; + aOther.mMaxStacksCount = mMaxStacksCount; + mMaxStacksCount = maxStacksCount; +} + +void CombinedStacks::Clear() { + mNextIndex = 0; + mStacks.clear(); + mModules.clear(); +} + +JSObject* CreateJSStackObject(JSContext* cx, const CombinedStacks& stacks) { + JS::Rooted<JSObject*> ret(cx, JS_NewPlainObject(cx)); + if (!ret) { + return nullptr; + } + + JS::Rooted<JSObject*> moduleArray(cx, JS::NewArrayObject(cx, 0)); + if (!moduleArray) { + return nullptr; + } + bool ok = + JS_DefineProperty(cx, ret, "memoryMap", moduleArray, JSPROP_ENUMERATE); + if (!ok) { + return nullptr; + } + + const size_t moduleCount = stacks.GetModuleCount(); + for (size_t moduleIndex = 0; moduleIndex < moduleCount; ++moduleIndex) { + // Current module + const Telemetry::ProcessedStack::Module& module = + stacks.GetModule(moduleIndex); + + JS::Rooted<JSObject*> moduleInfoArray(cx, JS::NewArrayObject(cx, 0)); + if (!moduleInfoArray) { + return nullptr; + } + if (!JS_DefineElement(cx, moduleArray, moduleIndex, moduleInfoArray, + JSPROP_ENUMERATE)) { + return nullptr; + } + + unsigned index = 0; + + // Module name + JS::Rooted<JSString*> str(cx, JS_NewUCStringCopyZ(cx, module.mName.get())); + if (!str || !JS_DefineElement(cx, moduleInfoArray, index++, str, + JSPROP_ENUMERATE)) { + return nullptr; + } + + // Module breakpad identifier + JS::Rooted<JSString*> id(cx, + JS_NewStringCopyZ(cx, module.mBreakpadId.get())); + if (!id || + !JS_DefineElement(cx, moduleInfoArray, index, id, JSPROP_ENUMERATE)) { + return nullptr; + } + } + + JS::Rooted<JSObject*> reportArray(cx, JS::NewArrayObject(cx, 0)); + if (!reportArray) { + return nullptr; + } + ok = JS_DefineProperty(cx, ret, "stacks", reportArray, JSPROP_ENUMERATE); + if (!ok) { + return nullptr; + } + + const size_t length = stacks.GetStackCount(); + for (size_t i = 0; i < length; ++i) { + // Represent call stack PCs as (module index, offset) pairs. + JS::Rooted<JSObject*> pcArray(cx, JS::NewArrayObject(cx, 0)); + if (!pcArray) { + return nullptr; + } + + if (!JS_DefineElement(cx, reportArray, i, pcArray, JSPROP_ENUMERATE)) { + return nullptr; + } + + const CombinedStacks::Stack& stack = stacks.GetStack(i); + const uint32_t pcCount = stack.size(); + for (size_t pcIndex = 0; pcIndex < pcCount; ++pcIndex) { + const Telemetry::ProcessedStack::Frame& frame = stack[pcIndex]; + JS::Rooted<JSObject*> framePair(cx, JS::NewArrayObject(cx, 0)); + if (!framePair) { + return nullptr; + } + int modIndex = (std::numeric_limits<uint16_t>::max() == frame.mModIndex) + ? -1 + : frame.mModIndex; + if (!JS_DefineElement(cx, framePair, 0, modIndex, JSPROP_ENUMERATE)) { + return nullptr; + } + if (!JS_DefineElement(cx, framePair, 1, + static_cast<double>(frame.mOffset), + JSPROP_ENUMERATE)) { + return nullptr; + } + if (!JS_DefineElement(cx, pcArray, pcIndex, framePair, + JSPROP_ENUMERATE)) { + return nullptr; + } + } + } + + return ret; +} + +} // namespace mozilla::Telemetry diff --git a/toolkit/components/telemetry/other/CombinedStacks.h b/toolkit/components/telemetry/other/CombinedStacks.h new file mode 100644 index 0000000000..6bd46823db --- /dev/null +++ b/toolkit/components/telemetry/other/CombinedStacks.h @@ -0,0 +1,108 @@ +/* -*- Mode: C++; tab-width: 8; 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/. */ + +#ifndef CombinedStacks_h__ +#define CombinedStacks_h__ + +#include <vector> + +#include "ipc/IPCMessageUtils.h" +#include "ProcessedStack.h" + +class JSObject; +struct JSContext; + +namespace mozilla { +namespace Telemetry { + +/** + * This class is conceptually a list of ProcessedStack objects, but it + * represents them more efficiently by keeping a single global list of modules. + */ +class CombinedStacks { + public: + explicit CombinedStacks(); + explicit CombinedStacks(size_t aMaxStacksCount); + + CombinedStacks(CombinedStacks&&) = default; + CombinedStacks& operator=(CombinedStacks&&) = default; + + void Swap(CombinedStacks& aOther); + + typedef std::vector<Telemetry::ProcessedStack::Frame> Stack; + const Telemetry::ProcessedStack::Module& GetModule(unsigned aIndex) const; + size_t GetModuleCount() const; + const Stack& GetStack(unsigned aIndex) const; + size_t AddStack(const Telemetry::ProcessedStack& aStack); + void AddStacks(const CombinedStacks& aStacks); + size_t GetStackCount() const; + size_t SizeOfExcludingThis() const; + void RemoveStack(unsigned aIndex); + size_t GetMaxStacksCount() const; + + /** Clears the contents of vectors and resets the index. */ + void Clear(); + + private: + std::vector<Telemetry::ProcessedStack::Module> mModules; + // A circular buffer to hold the stacks. + std::vector<Stack> mStacks; + // The index of the next buffer element to write to in mStacks. + size_t mNextIndex; + // The maximum number of stacks to keep in the CombinedStacks object. + size_t mMaxStacksCount; + + void AddFrame( + size_t aStackIndex, const ProcessedStack::Frame& aFrame, + const std::function<const ProcessedStack::Module&(int)>& aModuleGetter); + + friend struct ::IPC::ParamTraits<CombinedStacks>; +}; + +/** + * Creates a JSON representation of given combined stacks object. + */ +JSObject* CreateJSStackObject(JSContext* cx, const CombinedStacks& stacks); + +} // namespace Telemetry +} // namespace mozilla + +namespace IPC { + +template <> +struct ParamTraits<mozilla::Telemetry::CombinedStacks> { + typedef mozilla::Telemetry::CombinedStacks paramType; + + static void Write(MessageWriter* aWriter, const paramType& aParam) { + WriteParam(aWriter, aParam.mModules); + WriteParam(aWriter, aParam.mStacks); + WriteParam(aWriter, aParam.mNextIndex); + WriteParam(aWriter, aParam.mMaxStacksCount); + } + + static bool Read(MessageReader* aReader, paramType* aResult) { + if (!ReadParam(aReader, &aResult->mModules)) { + return false; + } + + if (!ReadParam(aReader, &aResult->mStacks)) { + return false; + } + + if (!ReadParam(aReader, &aResult->mNextIndex)) { + return false; + } + + if (!ReadParam(aReader, &aResult->mMaxStacksCount)) { + return false; + } + + return true; + } +}; + +} // namespace IPC + +#endif // CombinedStacks_h__ diff --git a/toolkit/components/telemetry/other/ProcessedStack.cpp b/toolkit/components/telemetry/other/ProcessedStack.cpp new file mode 100644 index 0000000000..ce62826a05 --- /dev/null +++ b/toolkit/components/telemetry/other/ProcessedStack.cpp @@ -0,0 +1,188 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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 "ProcessedStack.h" + +namespace { + +struct StackFrame { + uintptr_t mPC; // The program counter at this position in the call stack. + uint16_t mIndex; // The number of this frame in the call stack. + uint16_t mModIndex; // The index of module that has this program counter. +}; + +#ifdef MOZ_GECKO_PROFILER +static bool CompareByPC(const StackFrame& a, const StackFrame& b) { + return a.mPC < b.mPC; +} + +static bool CompareByIndex(const StackFrame& a, const StackFrame& b) { + return a.mIndex < b.mIndex; +} +#endif + +} // namespace + +namespace mozilla::Telemetry { + +const size_t kMaxChromeStackDepth = 50; + +ProcessedStack::ProcessedStack() = default; + +size_t ProcessedStack::GetStackSize() const { return mStack.size(); } + +size_t ProcessedStack::GetNumModules() const { return mModules.size(); } + +bool ProcessedStack::Module::operator==(const Module& aOther) const { + return mName == aOther.mName && mBreakpadId == aOther.mBreakpadId; +} + +const ProcessedStack::Frame& ProcessedStack::GetFrame(unsigned aIndex) const { + MOZ_ASSERT(aIndex < mStack.size()); + return mStack[aIndex]; +} + +void ProcessedStack::AddFrame(const Frame& aFrame) { mStack.push_back(aFrame); } + +const ProcessedStack::Module& ProcessedStack::GetModule(unsigned aIndex) const { + MOZ_ASSERT(aIndex < mModules.size()); + return mModules[aIndex]; +} + +void ProcessedStack::AddModule(const Module& aModule) { + mModules.push_back(aModule); +} + +void ProcessedStack::Clear() { + mModules.clear(); + mStack.clear(); +} + +ProcessedStack GetStackAndModules(const std::vector<uintptr_t>& aPCs) { + return BatchProcessedStackGenerator().GetStackAndModules(aPCs); +} + +BatchProcessedStackGenerator::BatchProcessedStackGenerator() +#ifdef MOZ_GECKO_PROFILER + : mSortedRawModules(SharedLibraryInfo::GetInfoForSelf()) +#endif +{ +#ifdef MOZ_GECKO_PROFILER + mSortedRawModules.SortByAddress(); +#endif +} + +#ifndef MOZ_GECKO_PROFILER +static ProcessedStack GetStackAndModulesInternal( + std::vector<StackFrame>& aRawStack) { +#else +static ProcessedStack GetStackAndModulesInternal( + std::vector<StackFrame>& aRawStack, SharedLibraryInfo& aSortedRawModules) { + SharedLibraryInfo rawModules(aSortedRawModules); + // Remove all modules not referenced by a PC on the stack + std::sort(aRawStack.begin(), aRawStack.end(), CompareByPC); + + size_t moduleIndex = 0; + size_t stackIndex = 0; + size_t stackSize = aRawStack.size(); + + while (moduleIndex < rawModules.GetSize()) { + const SharedLibrary& module = rawModules.GetEntry(moduleIndex); + uintptr_t moduleStart = module.GetStart(); + uintptr_t moduleEnd = module.GetEnd() - 1; + // the interval is [moduleStart, moduleEnd) + + bool moduleReferenced = false; + for (; stackIndex < stackSize; ++stackIndex) { + uintptr_t pc = aRawStack[stackIndex].mPC; + if (pc >= moduleEnd) break; + + if (pc >= moduleStart) { + // If the current PC is within the current module, mark + // module as used + moduleReferenced = true; + aRawStack[stackIndex].mPC -= moduleStart; + aRawStack[stackIndex].mModIndex = moduleIndex; + } else { + // PC does not belong to any module. It is probably from + // the JIT. Use a fixed mPC so that we don't get different + // stacks on different runs. + aRawStack[stackIndex].mPC = std::numeric_limits<uintptr_t>::max(); + } + } + + if (moduleReferenced) { + ++moduleIndex; + } else { + // Remove module if no PCs within its address range + rawModules.RemoveEntries(moduleIndex, moduleIndex + 1); + } + } + + for (; stackIndex < stackSize; ++stackIndex) { + // These PCs are past the last module. + aRawStack[stackIndex].mPC = std::numeric_limits<uintptr_t>::max(); + } + + std::sort(aRawStack.begin(), aRawStack.end(), CompareByIndex); +#endif + + // Copy the information to the return value. + ProcessedStack Ret; + for (auto& rawFrame : aRawStack) { + mozilla::Telemetry::ProcessedStack::Frame frame = {rawFrame.mPC, + rawFrame.mModIndex}; + Ret.AddFrame(frame); + } + +#ifdef MOZ_GECKO_PROFILER + for (unsigned i = 0, n = rawModules.GetSize(); i != n; ++i) { + const SharedLibrary& info = rawModules.GetEntry(i); + mozilla::Telemetry::ProcessedStack::Module module = {info.GetDebugName(), + info.GetBreakpadId()}; + Ret.AddModule(module); + } +#endif + + return Ret; +} + +ProcessedStack BatchProcessedStackGenerator::GetStackAndModules( + const std::vector<uintptr_t>& aPCs) { + std::vector<StackFrame> rawStack; + auto stackEnd = aPCs.begin() + std::min(aPCs.size(), kMaxChromeStackDepth); + for (auto i = aPCs.begin(); i != stackEnd; ++i) { + uintptr_t aPC = *i; + StackFrame Frame = {aPC, static_cast<uint16_t>(rawStack.size()), + std::numeric_limits<uint16_t>::max()}; + rawStack.push_back(Frame); + } + +#if defined(MOZ_GECKO_PROFILER) + return GetStackAndModulesInternal(rawStack, mSortedRawModules); +#else + return GetStackAndModulesInternal(rawStack); +#endif +} + +ProcessedStack BatchProcessedStackGenerator::GetStackAndModules( + const uintptr_t* aBegin, const uintptr_t* aEnd) { + std::vector<StackFrame> rawStack; + for (auto i = aBegin; i != aEnd; ++i) { + uintptr_t aPC = *i; + StackFrame Frame = {aPC, static_cast<uint16_t>(rawStack.size()), + std::numeric_limits<uint16_t>::max()}; + rawStack.push_back(Frame); + } + +#if defined(MOZ_GECKO_PROFILER) + return GetStackAndModulesInternal(rawStack, mSortedRawModules); +#else + return GetStackAndModulesInternal(rawStack); +#endif +} + +} // namespace mozilla::Telemetry diff --git a/toolkit/components/telemetry/other/ProcessedStack.h b/toolkit/components/telemetry/other/ProcessedStack.h new file mode 100644 index 0000000000..e289fe3fe6 --- /dev/null +++ b/toolkit/components/telemetry/other/ProcessedStack.h @@ -0,0 +1,137 @@ +/* -*- 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/. */ + +#ifndef ProcessedStack_h__ +#define ProcessedStack_h__ + +#include <vector> + +#include "ipc/IPCMessageUtils.h" +#include "ipc/IPCMessageUtilsSpecializations.h" +#include "mozilla/ipc/MessageChannel.h" +#include "mozilla/Vector.h" +#include "nsStringFwd.h" +#if defined(MOZ_GECKO_PROFILER) +# include "shared-libraries.h" +#endif // MOZ_GECKO_PROFILER + +namespace mozilla { +namespace Telemetry { + +// This class represents a stack trace and the modules referenced in that trace. +// It is designed to be easy to read and write to disk or network and doesn't +// include any logic on how to collect or read the information it stores. +class ProcessedStack { + public: + ProcessedStack(); + size_t GetStackSize() const; + size_t GetNumModules() const; + + struct Frame { + // The offset of this program counter in its module or an absolute pc. + uintptr_t mOffset; + // The index to pass to GetModule to get the module this program counter + // was in. + uint16_t mModIndex; + }; + struct Module { + // The file name, /foo/bar/libxul.so for example. + // It can contain unicode characters. + nsString mName; + nsCString mBreakpadId; + + bool operator==(const Module& other) const; + }; + + const Frame& GetFrame(unsigned aIndex) const; + void AddFrame(const Frame& aFrame); + const Module& GetModule(unsigned aIndex) const; + void AddModule(const Module& aFrame); + + void Clear(); + + private: + std::vector<Module> mModules; + std::vector<Frame> mStack; +}; + +// Get the current list of loaded modules, filter and pair it to the provided +// stack. We let the caller collect the stack since different callers have +// different needs (current thread X main thread, stopping the thread, etc). +ProcessedStack GetStackAndModules(const std::vector<uintptr_t>& aPCs); + +// This class optimizes repeated calls to GetStackAndModules. +class BatchProcessedStackGenerator { + public: + BatchProcessedStackGenerator(); + ProcessedStack GetStackAndModules(const std::vector<uintptr_t>& aPCs); + + template <typename AllocatorPolicy> + ProcessedStack GetStackAndModules( + const Vector<void*, 0, AllocatorPolicy>& aPCs) { + return GetStackAndModules(reinterpret_cast<const uintptr_t*>(aPCs.begin()), + reinterpret_cast<const uintptr_t*>(aPCs.end())); + } + + private: + ProcessedStack GetStackAndModules(const uintptr_t* aBegin, + const uintptr_t* aEnd); +#if defined(MOZ_GECKO_PROFILER) + SharedLibraryInfo mSortedRawModules; +#endif +}; + +} // namespace Telemetry +} // namespace mozilla + +namespace IPC { + +template <> +struct ParamTraits<mozilla::Telemetry::ProcessedStack::Module> { + typedef mozilla::Telemetry::ProcessedStack::Module paramType; + + static void Write(MessageWriter* aWriter, const paramType& aParam) { + WriteParam(aWriter, aParam.mName); + WriteParam(aWriter, aParam.mBreakpadId); + } + + static bool Read(MessageReader* aReader, paramType* aResult) { + if (!ReadParam(aReader, &aResult->mName)) { + return false; + } + + if (!ReadParam(aReader, &aResult->mBreakpadId)) { + return false; + } + + return true; + } +}; + +template <> +struct ParamTraits<mozilla::Telemetry::ProcessedStack::Frame> { + typedef mozilla::Telemetry::ProcessedStack::Frame paramType; + + static void Write(MessageWriter* aWriter, const paramType& aParam) { + WriteParam(aWriter, aParam.mOffset); + WriteParam(aWriter, aParam.mModIndex); + } + + static bool Read(MessageReader* aReader, paramType* aResult) { + if (!ReadParam(aReader, &aResult->mOffset)) { + return false; + } + + if (!ReadParam(aReader, &aResult->mModIndex)) { + return false; + } + + return true; + } +}; + +} // namespace IPC + +#endif // ProcessedStack_h__ diff --git a/toolkit/components/telemetry/other/TelemetryIOInterposeObserver.cpp b/toolkit/components/telemetry/other/TelemetryIOInterposeObserver.cpp new file mode 100644 index 0000000000..c0792bb48c --- /dev/null +++ b/toolkit/components/telemetry/other/TelemetryIOInterposeObserver.cpp @@ -0,0 +1,182 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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 "TelemetryIOInterposeObserver.h" +#include "core/TelemetryCommon.h" +#include "js/Array.h" // JS::NewArrayObject +#include "js/PropertyAndElement.h" // JS_DefineUCProperty +#include "js/PropertyDescriptor.h" // JSPROP_ENUMERATE, JSPROP_READONLY +#include "js/ValueArray.h" +#include "nsIFile.h" + +namespace mozilla::Telemetry { + +TelemetryIOInterposeObserver::TelemetryIOInterposeObserver(nsIFile* aXreDir) + : mCurStage(STAGE_STARTUP) { + nsAutoString xreDirPath; + nsresult rv = aXreDir->GetPath(xreDirPath); + if (NS_SUCCEEDED(rv)) { + AddPath(xreDirPath, u"{xre}"_ns); + } +} + +void TelemetryIOInterposeObserver::AddPath(const nsAString& aPath, + const nsAString& aSubstName) { + mSafeDirs.AppendElement(SafeDir(aPath, aSubstName)); +} + +// Threshold for reporting slow main-thread I/O (50 milliseconds). +const TimeDuration kTelemetryReportThreshold = + TimeDuration::FromMilliseconds(50); + +void TelemetryIOInterposeObserver::Observe(Observation& aOb) { + // We only report main-thread I/O + if (!IsMainThread()) { + return; + } + + if (aOb.ObservedOperation() == OpNextStage) { + mCurStage = NextStage(mCurStage); + MOZ_ASSERT(mCurStage < NUM_STAGES); + return; + } + + if (aOb.Duration() < kTelemetryReportThreshold) { + return; + } + + // Get the filename + nsAutoString filename; + aOb.Filename(filename); + + // Discard observations without filename + if (filename.IsEmpty()) { + return; + } + +#if defined(XP_WIN) + auto comparator = nsCaseInsensitiveStringComparator; +#else + auto comparator = nsTDefaultStringComparator<char16_t>; +#endif + nsAutoString processedName; + uint32_t safeDirsLen = mSafeDirs.Length(); + for (uint32_t i = 0; i < safeDirsLen; ++i) { + if (StringBeginsWith(filename, mSafeDirs[i].mPath, comparator)) { + processedName = mSafeDirs[i].mSubstName; + processedName += Substring(filename, mSafeDirs[i].mPath.Length()); + break; + } + } + + if (processedName.IsEmpty()) { + return; + } + + // Create a new entry or retrieve the existing one + FileIOEntryType* entry = mFileStats.PutEntry(processedName); + if (entry) { + FileStats& stats = entry->GetModifiableData()->mStats[mCurStage]; + // Update the statistics + stats.totalTime += (double)aOb.Duration().ToMilliseconds(); + switch (aOb.ObservedOperation()) { + case OpCreateOrOpen: + stats.creates++; + break; + case OpRead: + stats.reads++; + break; + case OpWrite: + stats.writes++; + break; + case OpFSync: + stats.fsyncs++; + break; + case OpStat: + stats.stats++; + break; + default: + break; + } + } +} + +bool TelemetryIOInterposeObserver::ReflectFileStats(FileIOEntryType* entry, + JSContext* cx, + JS::Handle<JSObject*> obj) { + JS::RootedValueArray<NUM_STAGES> stages(cx); + + FileStatsByStage& statsByStage = *entry->GetModifiableData(); + for (int s = STAGE_STARTUP; s < NUM_STAGES; ++s) { + FileStats& fileStats = statsByStage.mStats[s]; + + if (fileStats.totalTime == 0 && fileStats.creates == 0 && + fileStats.reads == 0 && fileStats.writes == 0 && + fileStats.fsyncs == 0 && fileStats.stats == 0) { + // Don't add an array that contains no information + stages[s].setNull(); + continue; + } + + // Array we want to report + JS::RootedValueArray<6> stats(cx); + stats[0].setNumber(fileStats.totalTime); + stats[1].setNumber(fileStats.creates); + stats[2].setNumber(fileStats.reads); + stats[3].setNumber(fileStats.writes); + stats[4].setNumber(fileStats.fsyncs); + stats[5].setNumber(fileStats.stats); + + // Create jsStats as array of elements above + JS::Rooted<JSObject*> jsStats(cx, JS::NewArrayObject(cx, stats)); + if (!jsStats) { + continue; + } + + stages[s].setObject(*jsStats); + } + + JS::Rooted<JSObject*> jsEntry(cx, JS::NewArrayObject(cx, stages)); + if (!jsEntry) { + return false; + } + + // Add jsEntry to top-level dictionary + const nsAString& key = entry->GetKey(); + return JS_DefineUCProperty(cx, obj, key.Data(), key.Length(), jsEntry, + JSPROP_ENUMERATE | JSPROP_READONLY); +} + +bool TelemetryIOInterposeObserver::ReflectIntoJS( + JSContext* cx, JS::Handle<JSObject*> rootObj) { + return mFileStats.ReflectIntoJS(ReflectFileStats, cx, rootObj); +} + +/** + * Get size of hash table with file stats + */ + +size_t TelemetryIOInterposeObserver::SizeOfIncludingThis( + mozilla::MallocSizeOf aMallocSizeOf) const { + return aMallocSizeOf(this) + SizeOfExcludingThis(aMallocSizeOf); +} + +size_t TelemetryIOInterposeObserver::SizeOfExcludingThis( + mozilla::MallocSizeOf aMallocSizeOf) const { + size_t size = 0; + size += mFileStats.ShallowSizeOfExcludingThis(aMallocSizeOf); + for (auto iter = mFileStats.ConstIter(); !iter.Done(); iter.Next()) { + size += iter.Get()->GetKey().SizeOfExcludingThisIfUnshared(aMallocSizeOf); + } + size += mSafeDirs.ShallowSizeOfExcludingThis(aMallocSizeOf); + uint32_t safeDirsLen = mSafeDirs.Length(); + for (uint32_t i = 0; i < safeDirsLen; ++i) { + size += mSafeDirs[i].SizeOfExcludingThis(aMallocSizeOf); + } + return size; +} + +} // namespace mozilla::Telemetry diff --git a/toolkit/components/telemetry/other/TelemetryIOInterposeObserver.h b/toolkit/components/telemetry/other/TelemetryIOInterposeObserver.h new file mode 100644 index 0000000000..54e3caf9b4 --- /dev/null +++ b/toolkit/components/telemetry/other/TelemetryIOInterposeObserver.h @@ -0,0 +1,116 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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/. */ + +/** + * IOInterposeObserver recording statistics of main-thread I/O during execution, + * aimed at consumption by TelemetryImpl + */ + +#ifndef TelemetryIOInterposeObserver_h__ +#define TelemetryIOInterposeObserver_h__ + +#include "core/TelemetryCommon.h" +#include "js/RootingAPI.h" +#include "js/TypeDecls.h" +#include "mozilla/IOInterposer.h" +#include "nsBaseHashtable.h" +#include "nsHashKeys.h" +#include "nsTArray.h" + +namespace mozilla { +namespace Telemetry { + +class TelemetryIOInterposeObserver : public IOInterposeObserver { + /** File-level statistics structure */ + struct FileStats { + FileStats() + : creates(0), reads(0), writes(0), fsyncs(0), stats(0), totalTime(0) {} + uint32_t creates; /** Number of create/open operations */ + uint32_t reads; /** Number of read operations */ + uint32_t writes; /** Number of write operations */ + uint32_t fsyncs; /** Number of fsync operations */ + uint32_t stats; /** Number of stat operations */ + double totalTime; /** Accumulated duration of all operations */ + }; + + struct SafeDir { + SafeDir(const nsAString& aPath, const nsAString& aSubstName) + : mPath(aPath), mSubstName(aSubstName) {} + size_t SizeOfExcludingThis(mozilla::MallocSizeOf aMallocSizeOf) const { + return mPath.SizeOfExcludingThisIfUnshared(aMallocSizeOf) + + mSubstName.SizeOfExcludingThisIfUnshared(aMallocSizeOf); + } + nsString mPath; /** Path to the directory */ + nsString mSubstName; /** Name to substitute with */ + }; + + public: + explicit TelemetryIOInterposeObserver(nsIFile* aXreDir); + + /** + * An implementation of Observe that records statistics of all + * file IO operations. + */ + void Observe(Observation& aOb) override; + + /** + * Reflect recorded file IO statistics into Javascript + */ + bool ReflectIntoJS(JSContext* cx, JS::Handle<JSObject*> rootObj); + + /** + * Adds a path for inclusion in main thread I/O report. + * @param aPath Directory path + * @param aSubstName Name to substitute for aPath for privacy reasons + */ + void AddPath(const nsAString& aPath, const nsAString& aSubstName); + + /** + * Get size of hash table with file stats + */ + size_t SizeOfIncludingThis(mozilla::MallocSizeOf aMallocSizeOf) const; + + size_t SizeOfExcludingThis(mozilla::MallocSizeOf aMallocSizeOf) const; + + private: + enum Stage { STAGE_STARTUP = 0, STAGE_NORMAL, STAGE_SHUTDOWN, NUM_STAGES }; + static inline Stage NextStage(Stage aStage) { + switch (aStage) { + case STAGE_STARTUP: + return STAGE_NORMAL; + case STAGE_NORMAL: + return STAGE_SHUTDOWN; + case STAGE_SHUTDOWN: + return STAGE_SHUTDOWN; + default: + return NUM_STAGES; + } + } + + struct FileStatsByStage { + FileStats mStats[NUM_STAGES]; + }; + typedef nsBaseHashtableET<nsStringHashKey, FileStatsByStage> FileIOEntryType; + + // Statistics for each filename + Common::AutoHashtable<FileIOEntryType> mFileStats; + // Container for allowed directories + nsTArray<SafeDir> mSafeDirs; + Stage mCurStage; + + /** + * Reflect a FileIOEntryType object to a Javascript property on obj with + * filename as key containing array: + * [totalTime, creates, reads, writes, fsyncs, stats] + */ + static bool ReflectFileStats(FileIOEntryType* entry, JSContext* cx, + JS::Handle<JSObject*> obj); +}; + +} // namespace Telemetry +} // namespace mozilla + +#endif // TelemetryIOInterposeObserver_h__ diff --git a/toolkit/components/telemetry/other/UntrustedModules.cpp b/toolkit/components/telemetry/other/UntrustedModules.cpp new file mode 100644 index 0000000000..fc988c2d06 --- /dev/null +++ b/toolkit/components/telemetry/other/UntrustedModules.cpp @@ -0,0 +1,293 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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 "UntrustedModules.h" + +#include "mozilla/dom/ContentParent.h" +#include "mozilla/MozPromise.h" +#include "mozilla/net/SocketProcessParent.h" +#include "mozilla/ipc/UtilityProcessParent.h" +#include "mozilla/ipc/UtilityProcessManager.h" +#include "mozilla/RDDChild.h" +#include "mozilla/RDDProcessManager.h" +#include "mozilla/WinDllServices.h" +#include "nsISupportsImpl.h" +#include "nsProxyRelease.h" +#include "nsXULAppAPI.h" +#include "UntrustedModulesDataSerializer.h" + +namespace mozilla { +namespace Telemetry { + +static const uint32_t kMaxModulesArrayLen = 100; + +using UntrustedModulesIpcPromise = + MozPromise<Maybe<UntrustedModulesData>, ipc::ResponseRejectReason, true>; + +using MultiGetUntrustedModulesPromise = + MozPromise<bool /*aIgnored*/, nsresult, true>; + +class MOZ_HEAP_CLASS MultiGetUntrustedModulesData final { + public: + /** + * @param aFlags [in] Combinations of the flags defined under nsITelemetry. + * (See "Flags for getUntrustedModuleLoadEvents" + * in nsITelemetry.idl) + */ + explicit MultiGetUntrustedModulesData(uint32_t aFlags) + : mFlags(aFlags), + mBackupSvc(UntrustedModulesBackupService::Get()), + mPromise(new MultiGetUntrustedModulesPromise::Private(__func__)), + mNumPending(0) {} + + NS_INLINE_DECL_THREADSAFE_REFCOUNTING(MultiGetUntrustedModulesData) + + RefPtr<MultiGetUntrustedModulesPromise> GetUntrustedModuleLoadEvents(); + void Serialize(RefPtr<dom::Promise>&& aPromise); + + MultiGetUntrustedModulesData(const MultiGetUntrustedModulesData&) = delete; + MultiGetUntrustedModulesData(MultiGetUntrustedModulesData&&) = delete; + MultiGetUntrustedModulesData& operator=(const MultiGetUntrustedModulesData&) = + delete; + MultiGetUntrustedModulesData& operator=(MultiGetUntrustedModulesData&&) = + delete; + + private: + ~MultiGetUntrustedModulesData() = default; + + void AddPending(RefPtr<UntrustedModulesPromise>&& aNewPending) { + MOZ_ASSERT(NS_IsMainThread()); + + ++mNumPending; + + RefPtr<MultiGetUntrustedModulesData> self(this); + aNewPending->Then( + GetMainThreadSerialEventTarget(), __func__, + [self](Maybe<UntrustedModulesData>&& aResult) { + self->OnCompletion(std::move(aResult)); + }, + [self](nsresult aReason) { self->OnCompletion(); }); + } + + void AddPending(RefPtr<UntrustedModulesIpcPromise>&& aNewPending) { + MOZ_ASSERT(NS_IsMainThread()); + + ++mNumPending; + + RefPtr<MultiGetUntrustedModulesData> self(this); + aNewPending->Then( + GetMainThreadSerialEventTarget(), __func__, + [self](Maybe<UntrustedModulesData>&& aResult) { + self->OnCompletion(std::move(aResult)); + }, + [self](ipc::ResponseRejectReason&& aReason) { self->OnCompletion(); }); + } + + void OnCompletion() { + MOZ_ASSERT(NS_IsMainThread() && mNumPending > 0); + + --mNumPending; + if (mNumPending) { + return; + } + + mPromise->Resolve(true, __func__); + } + + void OnCompletion(Maybe<UntrustedModulesData>&& aResult) { + MOZ_ASSERT(NS_IsMainThread()); + + if (aResult.isSome()) { + mBackupSvc->Backup(std::move(aResult.ref())); + } + + OnCompletion(); + } + + private: + // Combinations of the flags defined under nsITelemetry. + // (See "Flags for getUntrustedModuleLoadEvents" in nsITelemetry.idl) + uint32_t mFlags; + + RefPtr<UntrustedModulesBackupService> mBackupSvc; + RefPtr<MultiGetUntrustedModulesPromise::Private> mPromise; + size_t mNumPending; +}; + +RefPtr<MultiGetUntrustedModulesPromise> +MultiGetUntrustedModulesData::GetUntrustedModuleLoadEvents() { + MOZ_ASSERT(XRE_IsParentProcess() && NS_IsMainThread()); + + // Parent process + RefPtr<DllServices> dllSvc(DllServices::Get()); + AddPending(dllSvc->GetUntrustedModulesData()); + + // Child processes + nsTArray<dom::ContentParent*> contentParents; + dom::ContentParent::GetAll(contentParents); + for (auto&& contentParent : contentParents) { + AddPending(contentParent->SendGetUntrustedModulesData()); + } + + if (auto* socketActor = net::SocketProcessParent::GetSingleton()) { + AddPending(socketActor->SendGetUntrustedModulesData()); + } + + if (RDDProcessManager* rddMgr = RDDProcessManager::Get()) { + if (RDDChild* rddChild = rddMgr->GetRDDChild()) { + AddPending(rddChild->SendGetUntrustedModulesData()); + } + } + + if (RefPtr<ipc::UtilityProcessManager> utilityManager = + ipc::UtilityProcessManager::GetIfExists()) { + for (RefPtr<ipc::UtilityProcessParent>& parent : + utilityManager->GetAllProcessesProcessParent()) { + AddPending(parent->SendGetUntrustedModulesData()); + } + } + + return mPromise; +} + +void MultiGetUntrustedModulesData::Serialize(RefPtr<dom::Promise>&& aPromise) { + MOZ_ASSERT(NS_IsMainThread()); + + dom::AutoJSAPI jsapi; + if (NS_WARN_IF(!jsapi.Init(aPromise->GetGlobalObject()))) { + aPromise->MaybeReject(NS_ERROR_FAILURE); + return; + } + + JSContext* cx = jsapi.cx(); + UntrustedModulesDataSerializer serializer(cx, kMaxModulesArrayLen, mFlags); + if (!serializer) { + aPromise->MaybeReject(NS_ERROR_FAILURE); + return; + } + + nsresult rv; + if (mFlags & nsITelemetry::INCLUDE_OLD_LOADEVENTS) { + // When INCLUDE_OLD_LOADEVENTS is set, we need to return instances + // from both "Staging" and "Settled" backup. + if (mFlags & nsITelemetry::KEEP_LOADEVENTS_NEW) { + // When INCLUDE_OLD_LOADEVENTS and KEEP_LOADEVENTS_NEW are set, we need to + // return a JS object consisting of all instances from both "Staging" and + // "Settled" backups, keeping instances in those backups as is. + if (mFlags & nsITelemetry::EXCLUDE_STACKINFO_FROM_LOADEVENTS) { + // Without the stack info, we can add multiple UntrustedModulesData to + // the serializer directly. + rv = serializer.Add(mBackupSvc->Staging()); + if (NS_WARN_IF(NS_FAILED(rv))) { + aPromise->MaybeReject(rv); + return; + } + rv = serializer.Add(mBackupSvc->Settled()); + if (NS_WARN_IF(NS_FAILED(rv))) { + aPromise->MaybeReject(rv); + return; + } + } else { + // Currently we don't have a method to merge UntrustedModulesData into + // a serialized JS object because merging CombinedStack will be tricky. + // Thus we return an error on this flag combination. + aPromise->MaybeReject(NS_ERROR_INVALID_ARG); + return; + } + } else { + // When KEEP_LOADEVENTS_NEW is not set, we can move data from "Staging" + // to "Settled" first, then add "Settled" to the serializer. + mBackupSvc->SettleAllStagingData(); + + const UntrustedModulesBackupData& settledRef = mBackupSvc->Settled(); + if (settledRef.IsEmpty()) { + aPromise->MaybeReject(NS_ERROR_NOT_AVAILABLE); + return; + } + + rv = serializer.Add(settledRef); + if (NS_WARN_IF(NS_FAILED(rv))) { + aPromise->MaybeReject(rv); + return; + } + } + } else { + // When INCLUDE_OLD_LOADEVENTS is not set, we serialize only the "Staging" + // into a JS object. + const UntrustedModulesBackupData& stagingRef = mBackupSvc->Staging(); + + if (stagingRef.IsEmpty()) { + aPromise->MaybeReject(NS_ERROR_NOT_AVAILABLE); + return; + } + + rv = serializer.Add(stagingRef); + if (NS_WARN_IF(NS_FAILED(rv))) { + aPromise->MaybeReject(rv); + return; + } + + // When KEEP_LOADEVENTS_NEW is not set, we move all "Staging" instances + // to the "Settled". + if (!(mFlags & nsITelemetry::KEEP_LOADEVENTS_NEW)) { + mBackupSvc->SettleAllStagingData(); + } + } + +#if defined(XP_WIN) + RefPtr<DllServices> dllSvc(DllServices::Get()); + nt::SharedSection* sharedSection = dllSvc->GetSharedSection(); + if (sharedSection) { + auto dynamicBlocklist = sharedSection->GetDynamicBlocklist(); + + nsTArray<nsDependentSubstring> blockedModules; + for (const auto& blockedEntry : dynamicBlocklist) { + if (!blockedEntry.IsValidDynamicBlocklistEntry()) { + break; + } + blockedModules.AppendElement( + nsDependentSubstring(blockedEntry.mName.Buffer, + blockedEntry.mName.Length / sizeof(wchar_t))); + } + rv = serializer.AddBlockedModules(blockedModules); + if (NS_WARN_IF(NS_FAILED(rv))) { + aPromise->MaybeReject(rv); + return; + } + } +#endif + + JS::Rooted<JS::Value> jsval(cx); + serializer.GetObject(&jsval); + aPromise->MaybeResolve(jsval); +} + +nsresult GetUntrustedModuleLoadEvents(uint32_t aFlags, JSContext* cx, + dom::Promise** aPromise) { + // Create a promise using global context. + nsIGlobalObject* global = xpc::CurrentNativeGlobal(cx); + if (NS_WARN_IF(!global)) { + return NS_ERROR_FAILURE; + } + + ErrorResult result; + RefPtr<dom::Promise> promise(dom::Promise::Create(global, result)); + if (NS_WARN_IF(result.Failed())) { + return result.StealNSResult(); + } + + auto multi = MakeRefPtr<MultiGetUntrustedModulesData>(aFlags); + multi->GetUntrustedModuleLoadEvents()->Then( + GetMainThreadSerialEventTarget(), __func__, + [promise, multi](bool) mutable { multi->Serialize(std::move(promise)); }, + [promise](nsresult aRv) { promise->MaybeReject(aRv); }); + + promise.forget(aPromise); + return NS_OK; +} + +} // namespace Telemetry +} // namespace mozilla diff --git a/toolkit/components/telemetry/other/UntrustedModules.h b/toolkit/components/telemetry/other/UntrustedModules.h new file mode 100644 index 0000000000..3bb53c1f9a --- /dev/null +++ b/toolkit/components/telemetry/other/UntrustedModules.h @@ -0,0 +1,31 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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/. */ + +#ifndef telemetry_UntrustedModules_h__ +#define telemetry_UntrustedModules_h__ + +#include "jsapi.h" +#include "mozilla/dom/Promise.h" + +namespace mozilla { +namespace Telemetry { + +/** + * This function returns a promise that asynchronously processes and gathers + * untrusted module data. The promise is either resolved with the JS object + * ping payload, or is rejected upon failure. + * + * @param aFlags [in] Combinations of the flags defined under nsITelemetry. + * (See "Flags for getUntrustedModuleLoadEvents" + * in nsITelemetry.idl) + */ +nsresult GetUntrustedModuleLoadEvents(uint32_t aFlags, JSContext* cx, + dom::Promise** aPromise); + +} // namespace Telemetry +} // namespace mozilla + +#endif // telemetry_UntrustedModules_h__ diff --git a/toolkit/components/telemetry/other/UntrustedModulesBackupService.cpp b/toolkit/components/telemetry/other/UntrustedModulesBackupService.cpp new file mode 100644 index 0000000000..a0638e5aec --- /dev/null +++ b/toolkit/components/telemetry/other/UntrustedModulesBackupService.cpp @@ -0,0 +1,97 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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 https://mozilla.org/MPL/2.0/. */ + +#include "UntrustedModulesBackupService.h" + +#include "mozilla/ClearOnShutdown.h" +#include "mozilla/HashFunctions.h" +#include "mozilla/SchedulerGroup.h" +#include "mozilla/StaticLocalPtr.h" + +namespace mozilla { + +ProcessHashKey::ProcessHashKey(GeckoProcessType aType, DWORD aPid) + : mType(aType), mPid(aPid) {} + +bool ProcessHashKey::operator==(const ProcessHashKey& aOther) const { + return mPid == aOther.mPid && mType == aOther.mType; +} + +PLDHashNumber ProcessHashKey::Hash() const { return HashGeneric(mPid, mType); } + +void UntrustedModulesBackupData::Add(UntrustedModulesData&& aData) { + WithEntryHandle( + ProcessHashKey(aData.mProcessType, aData.mPid), [&](auto&& p) { + if (p) { + p.Data()->mData.Merge(std::move(aData)); + } else { + p.Insert(MakeRefPtr<UntrustedModulesDataContainer>(std::move(aData))); + } + }); +} + +void UntrustedModulesBackupData::AddWithoutStacks( + UntrustedModulesData&& aData) { + WithEntryHandle( + ProcessHashKey(aData.mProcessType, aData.mPid), [&](auto&& p) { + if (p) { + p.Data()->mData.MergeWithoutStacks(std::move(aData)); + } else { + aData.Truncate(true); + p.Insert(MakeRefPtr<UntrustedModulesDataContainer>(std::move(aData))); + } + }); +} + +/* static */ +UntrustedModulesBackupService* UntrustedModulesBackupService::Get() { + if (!XRE_IsParentProcess()) { + return nullptr; + } + + static StaticLocalRefPtr<UntrustedModulesBackupService> sInstance( + []() -> already_AddRefed<UntrustedModulesBackupService> { + RefPtr<UntrustedModulesBackupService> instance( + new UntrustedModulesBackupService()); + + auto setClearOnShutdown = [ptr = &sInstance]() -> void { + ClearOnShutdown(ptr); + }; + + if (NS_IsMainThread()) { + setClearOnShutdown(); + return instance.forget(); + } + + SchedulerGroup::Dispatch( + TaskCategory::Other, + NS_NewRunnableFunction( + "mozilla::UntrustedModulesBackupService::Get", + std::move(setClearOnShutdown))); + + return instance.forget(); + }()); + + return sInstance; +} + +void UntrustedModulesBackupService::Backup(UntrustedModulesData&& aData) { + mStaging.Add(std::move(aData)); +} + +void UntrustedModulesBackupService::SettleAllStagingData() { + UntrustedModulesBackupData staging; + staging.SwapElements(mStaging); + + for (auto&& iter = staging.Iter(); !iter.Done(); iter.Next()) { + if (!iter.Data()) { + continue; + } + mSettled.AddWithoutStacks(std::move(iter.Data()->mData)); + } +} + +} // namespace mozilla diff --git a/toolkit/components/telemetry/other/UntrustedModulesBackupService.h b/toolkit/components/telemetry/other/UntrustedModulesBackupService.h new file mode 100644 index 0000000000..36e4cc7b1c --- /dev/null +++ b/toolkit/components/telemetry/other/UntrustedModulesBackupService.h @@ -0,0 +1,73 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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 https://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_UntrustedModulesBackupService_h +#define mozilla_UntrustedModulesBackupService_h + +#include "mozilla/UntrustedModulesData.h" +#include "nsRefPtrHashtable.h" + +namespace mozilla { + +struct ProcessHashKey { + GeckoProcessType mType; + DWORD mPid; + ProcessHashKey(GeckoProcessType aType, DWORD aPid); + bool operator==(const ProcessHashKey& aOther) const; + PLDHashNumber Hash() const; +}; + +// UntrustedModulesData should not be refcounted as it's exchanged via IPC. +// Instead, we define this container class owning UntrustedModulesData along +// with a refcount. +class MOZ_HEAP_CLASS UntrustedModulesDataContainer final { + ~UntrustedModulesDataContainer() = default; + + public: + UntrustedModulesData mData; + + explicit UntrustedModulesDataContainer(UntrustedModulesData&& aData) + : mData(std::move(aData)) {} + + NS_INLINE_DECL_THREADSAFE_REFCOUNTING(UntrustedModulesDataContainer) +}; + +class UntrustedModulesBackupData + : public nsRefPtrHashtable<nsGenericHashKey<ProcessHashKey>, + UntrustedModulesDataContainer> { + public: + void Add(UntrustedModulesData&& aData); + void AddWithoutStacks(UntrustedModulesData&& aData); +}; + +class MOZ_HEAP_CLASS UntrustedModulesBackupService final { + public: + NS_INLINE_DECL_THREADSAFE_REFCOUNTING(UntrustedModulesBackupService) + + static UntrustedModulesBackupService* Get(); + + // Back up data to mStaging + void Backup(UntrustedModulesData&& aData); + + void SettleAllStagingData(); + + const UntrustedModulesBackupData& Staging() const { return mStaging; } + const UntrustedModulesBackupData& Settled() const { return mSettled; } + + private: + // Data not yet submitted as telemetry + UntrustedModulesBackupData mStaging; + + // Data already submitted as telemetry + // (This does not have stack information) + UntrustedModulesBackupData mSettled; + + ~UntrustedModulesBackupService() = default; +}; + +} // namespace mozilla + +#endif // mozilla_UntrustedModulesBackupService_h diff --git a/toolkit/components/telemetry/other/UntrustedModulesDataSerializer.cpp b/toolkit/components/telemetry/other/UntrustedModulesDataSerializer.cpp new file mode 100644 index 0000000000..603b85fd88 --- /dev/null +++ b/toolkit/components/telemetry/other/UntrustedModulesDataSerializer.cpp @@ -0,0 +1,609 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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 "UntrustedModulesDataSerializer.h" + +#include "core/TelemetryCommon.h" +#include "js/Array.h" // JS::NewArrayObject +#include "js/PropertyAndElement.h" // JS_DefineElement, JS_DefineProperty, JS_GetProperty +#include "jsapi.h" +#include "mozilla/dom/ToJSValue.h" +#include "nsITelemetry.h" +#include "nsUnicharUtils.h" +#include "nsXULAppAPI.h" + +#if defined(MOZ_GECKO_PROFILER) +# include "shared-libraries.h" +#endif // MOZ_GECKO_PROFILER + +namespace mozilla { +namespace Telemetry { + +static const uint32_t kThirdPartyModulesPingVersion = 1; + +/** + * Limits the length of a string by removing the middle of the string, replacing + * with ellipsis. + * e.g. LimitStringLength("hello world", 6) would result in "he...d" + * + * @param aStr [in,out] The string to transform + * @param aMaxFieldLength [in] The maximum length of the resulting string. + */ +static void LimitStringLength(nsAString& aStr, size_t aMaxFieldLength) { + if (aStr.Length() <= aMaxFieldLength) { + return; + } + + constexpr auto kEllipsis = u"..."_ns; + + if (aMaxFieldLength <= (kEllipsis.Length() + 3)) { + // An ellipsis is useless in this case, as it would obscure the string to + // the point that we cannot even determine the string's contents. We might + // as well just truncate. + aStr.Truncate(aMaxFieldLength); + return; + } + + size_t cutPos = (aMaxFieldLength - kEllipsis.Length()) / 2; + size_t rightLen = aMaxFieldLength - kEllipsis.Length() - cutPos; + size_t cutLen = aStr.Length() - (cutPos + rightLen); + + aStr.Replace(cutPos, cutLen, kEllipsis); +} + +/** + * Adds a string property to a JS object, that's limited in length using + * LimitStringLength(). + * + * @param cx [in] The JS context + * @param aObj [in] The object to add the property to + * @param aName [in] The name of the property to add + * @param aVal [in] The JS value of the resulting property. + * @param aMaxFieldLength [in] The maximum length of the value + * (see LimitStringLength()) + * @return true upon success + */ +static bool AddLengthLimitedStringProp(JSContext* cx, + JS::Handle<JSObject*> aObj, + const char* aName, const nsAString& aVal, + size_t aMaxFieldLength = MAX_PATH) { + JS::Rooted<JS::Value> jsval(cx); + nsAutoString shortVal(aVal); + LimitStringLength(shortVal, aMaxFieldLength); + jsval.setString(Common::ToJSString(cx, shortVal)); + return JS_DefineProperty(cx, aObj, aName, jsval, JSPROP_ENUMERATE); +}; + +static JSString* ModuleVersionToJSString(JSContext* aCx, + const ModuleVersion& aVersion) { + auto [major, minor, patch, build] = aVersion.AsTuple(); + + constexpr auto dot = u"."_ns; + + nsAutoString strVer; + strVer.AppendInt(major); + strVer.Append(dot); + strVer.AppendInt(minor); + strVer.Append(dot); + strVer.AppendInt(patch); + strVer.Append(dot); + strVer.AppendInt(build); + + return Common::ToJSString(aCx, strVer); +} + +/** + * Convert the given container object to a JavaScript array. + * + * @param cx [in] The JS context. + * @param aRet [out] This gets assigned to the newly created + * array object. + * @param aContainer [in] The source container to convert. + * @param aElementConverter [in] A callable used to convert each element + * to a JS element. The form of this function is: + * bool(JSContext *cx, + * JS::MutableHandleValue aRet, + * const ElementT& aElement) + * @return true if aRet was successfully assigned to the new array object. + */ +template <typename T, typename Converter, typename... Args> +static bool ContainerToJSArray(JSContext* cx, JS::MutableHandle<JSObject*> aRet, + const T& aContainer, + Converter&& aElementConverter, Args&&... aArgs) { + JS::Rooted<JSObject*> arr(cx, JS::NewArrayObject(cx, 0)); + if (!arr) { + return false; + } + + size_t i = 0; + for (auto&& item : aContainer) { + JS::Rooted<JS::Value> jsel(cx); + if (!aElementConverter(cx, &jsel, *item, std::forward<Args>(aArgs)...)) { + return false; + } + if (!JS_DefineElement(cx, arr, i, jsel, JSPROP_ENUMERATE)) { + return false; + } + ++i; + } + + aRet.set(arr); + return true; +} + +static bool SerializeModule(JSContext* aCx, + JS::MutableHandle<JS::Value> aElement, + const RefPtr<ModuleRecord>& aModule, + uint32_t aFlags) { + if (!aModule) { + return false; + } + + JS::Rooted<JSObject*> obj(aCx, JS_NewPlainObject(aCx)); + if (!obj) { + return false; + } + + if (aFlags & nsITelemetry::INCLUDE_PRIVATE_FIELDS_IN_LOADEVENTS) { + JS::Rooted<JS::Value> jsFileObj(aCx); + if (!dom::ToJSValue(aCx, aModule->mResolvedDosName, &jsFileObj) || + !JS_DefineProperty(aCx, obj, "dllFile", jsFileObj, JSPROP_ENUMERATE)) { + return false; + } + } else { + if (!AddLengthLimitedStringProp(aCx, obj, "resolvedDllName", + aModule->mSanitizedDllName)) { + return false; + } + } + + if (aModule->mVersion.isSome()) { + JS::Rooted<JS::Value> jsModuleVersion(aCx); + jsModuleVersion.setString( + ModuleVersionToJSString(aCx, aModule->mVersion.ref())); + if (!JS_DefineProperty(aCx, obj, "fileVersion", jsModuleVersion, + JSPROP_ENUMERATE)) { + return false; + } + } + +#if defined(MOZ_GECKO_PROFILER) + if (aModule->mResolvedDosName) { + nsAutoString path; + if (aModule->mResolvedDosName->GetPath(path) == NS_OK) { + SharedLibraryInfo info = SharedLibraryInfo::GetInfoFromPath(path.Data()); + if (info.GetSize() > 0) { + nsCString breakpadId = info.GetEntry(0).GetBreakpadId(); + if (!AddLengthLimitedStringProp(aCx, obj, "debugID", + NS_ConvertASCIItoUTF16(breakpadId))) { + return false; + } + } + } + } +#endif // MOZ_GECKO_PROFILER + + if (aModule->mVendorInfo.isSome()) { + const char* propName; + + const VendorInfo& vendorInfo = aModule->mVendorInfo.ref(); + switch (vendorInfo.mSource) { + case VendorInfo::Source::Signature: + propName = "signedBy"; + break; + case VendorInfo::Source::VersionInfo: + propName = "companyName"; + break; + default: + MOZ_ASSERT_UNREACHABLE("Unknown VendorInfo Source!"); + return false; + } + + MOZ_ASSERT(!vendorInfo.mVendor.IsEmpty()); + if (vendorInfo.mVendor.IsEmpty()) { + return false; + } + + if (!AddLengthLimitedStringProp(aCx, obj, propName, vendorInfo.mVendor)) { + return false; + } + } + + JS::Rooted<JS::Value> jsTrustFlags(aCx); + jsTrustFlags.setNumber(static_cast<uint32_t>(aModule->mTrustFlags)); + if (!JS_DefineProperty(aCx, obj, "trustFlags", jsTrustFlags, + JSPROP_ENUMERATE)) { + return false; + } + + aElement.setObject(*obj); + return true; +} + +/* static */ +bool UntrustedModulesDataSerializer::SerializeEvent( + JSContext* aCx, JS::MutableHandle<JS::Value> aElement, + const ProcessedModuleLoadEventContainer& aEventContainer, + const IndexMap& aModuleIndices) { + MOZ_ASSERT(NS_IsMainThread()); + + const ProcessedModuleLoadEvent& event = aEventContainer.mEvent; + if (!event) { + return false; + } + + JS::Rooted<JSObject*> obj(aCx, JS_NewPlainObject(aCx)); + if (!obj) { + return false; + } + + JS::Rooted<JS::Value> jsProcessUptimeMS(aCx); + // Javascript doesn't like 64-bit integers; convert to double. + jsProcessUptimeMS.setNumber(static_cast<double>(event.mProcessUptimeMS)); + if (!JS_DefineProperty(aCx, obj, "processUptimeMS", jsProcessUptimeMS, + JSPROP_ENUMERATE)) { + return false; + } + + if (event.mLoadDurationMS) { + JS::Rooted<JS::Value> jsLoadDurationMS(aCx); + jsLoadDurationMS.setNumber(event.mLoadDurationMS.value()); + if (!JS_DefineProperty(aCx, obj, "loadDurationMS", jsLoadDurationMS, + JSPROP_ENUMERATE)) { + return false; + } + } + + JS::Rooted<JS::Value> jsThreadId(aCx); + jsThreadId.setNumber(static_cast<uint32_t>(event.mThreadId)); + if (!JS_DefineProperty(aCx, obj, "threadID", jsThreadId, JSPROP_ENUMERATE)) { + return false; + } + + nsDependentCString effectiveThreadName; + if (event.mThreadId == ::GetCurrentThreadId()) { + effectiveThreadName.Rebind("Main Thread"_ns, 0); + } else { + effectiveThreadName.Rebind(event.mThreadName, 0); + } + + if (!effectiveThreadName.IsEmpty()) { + JS::Rooted<JS::Value> jsThreadName(aCx); + jsThreadName.setString(Common::ToJSString(aCx, effectiveThreadName)); + if (!JS_DefineProperty(aCx, obj, "threadName", jsThreadName, + JSPROP_ENUMERATE)) { + return false; + } + } + + // Don't add this property unless mRequestedDllName differs from + // the associated module's mSanitizedDllName + if (!event.mRequestedDllName.IsEmpty() && + !event.mRequestedDllName.Equals(event.mModule->mSanitizedDllName, + nsCaseInsensitiveStringComparator)) { + if (!AddLengthLimitedStringProp(aCx, obj, "requestedDllName", + event.mRequestedDllName)) { + return false; + } + } + + nsAutoString strBaseAddress; + strBaseAddress.AppendLiteral(u"0x"); + strBaseAddress.AppendInt(event.mBaseAddress, 16); + + JS::Rooted<JS::Value> jsBaseAddress(aCx); + jsBaseAddress.setString(Common::ToJSString(aCx, strBaseAddress)); + if (!JS_DefineProperty(aCx, obj, "baseAddress", jsBaseAddress, + JSPROP_ENUMERATE)) { + return false; + } + + uint32_t index; + if (!aModuleIndices.Get(event.mModule->mResolvedNtName, &index)) { + return false; + } + + JS::Rooted<JS::Value> jsModuleIndex(aCx); + jsModuleIndex.setNumber(index); + if (!JS_DefineProperty(aCx, obj, "moduleIndex", jsModuleIndex, + JSPROP_ENUMERATE)) { + return false; + } + + JS::Rooted<JS::Value> jsIsDependent(aCx); + jsIsDependent.setBoolean(event.mIsDependent); + if (!JS_DefineProperty(aCx, obj, "isDependent", jsIsDependent, + JSPROP_ENUMERATE)) { + return false; + } + + JS::Rooted<JS::Value> jsLoadStatus(aCx); + jsLoadStatus.setNumber(event.mLoadStatus); + if (!JS_DefineProperty(aCx, obj, "loadStatus", jsLoadStatus, + JSPROP_ENUMERATE)) { + return false; + } + + aElement.setObject(*obj); + + return true; +} + +static nsDependentCString GetProcessTypeString(GeckoProcessType aType) { + nsDependentCString strProcType; + if (aType == GeckoProcessType_Default) { + strProcType.Rebind("browser"_ns, 0); + } else { + strProcType.Rebind(XRE_GeckoProcessTypeToString(aType)); + } + return strProcType; +} + +nsresult UntrustedModulesDataSerializer::GetPerProcObject( + const UntrustedModulesData& aData, JS::MutableHandle<JSObject*> aObj) { + JS::Rooted<JS::Value> jsProcType(mCx); + jsProcType.setString( + Common::ToJSString(mCx, GetProcessTypeString(aData.mProcessType))); + if (!JS_DefineProperty(mCx, aObj, "processType", jsProcType, + JSPROP_ENUMERATE)) { + return NS_ERROR_FAILURE; + } + + JS::Rooted<JS::Value> jsElapsed(mCx); + jsElapsed.setNumber(aData.mElapsed.ToSecondsSigDigits()); + if (!JS_DefineProperty(mCx, aObj, "elapsed", jsElapsed, JSPROP_ENUMERATE)) { + return NS_ERROR_FAILURE; + } + + if (aData.mXULLoadDurationMS.isSome()) { + JS::Rooted<JS::Value> jsXulLoadDurationMS(mCx); + jsXulLoadDurationMS.setNumber(aData.mXULLoadDurationMS.value()); + if (!JS_DefineProperty(mCx, aObj, "xulLoadDurationMS", jsXulLoadDurationMS, + JSPROP_ENUMERATE)) { + return NS_ERROR_FAILURE; + } + } + + JS::Rooted<JS::Value> jsSanitizationFailures(mCx); + jsSanitizationFailures.setNumber(aData.mSanitizationFailures); + if (!JS_DefineProperty(mCx, aObj, "sanitizationFailures", + jsSanitizationFailures, JSPROP_ENUMERATE)) { + return NS_ERROR_FAILURE; + } + + JS::Rooted<JS::Value> jsTrustTestFailures(mCx); + jsTrustTestFailures.setNumber(aData.mTrustTestFailures); + if (!JS_DefineProperty(mCx, aObj, "trustTestFailures", jsTrustTestFailures, + JSPROP_ENUMERATE)) { + return NS_ERROR_FAILURE; + } + + JS::Rooted<JSObject*> eventsArray(mCx); + if (!ContainerToJSArray(mCx, &eventsArray, aData.mEvents, &SerializeEvent, + mIndexMap)) { + return NS_ERROR_FAILURE; + } + + if (!JS_DefineProperty(mCx, aObj, "events", eventsArray, JSPROP_ENUMERATE)) { + return NS_ERROR_FAILURE; + } + + if (!(mFlags & nsITelemetry::EXCLUDE_STACKINFO_FROM_LOADEVENTS)) { + JS::Rooted<JSObject*> combinedStacksObj( + mCx, CreateJSStackObject(mCx, aData.mStacks)); + if (!combinedStacksObj) { + return NS_ERROR_FAILURE; + } + + if (!JS_DefineProperty(mCx, aObj, "combinedStacks", combinedStacksObj, + JSPROP_ENUMERATE)) { + return NS_ERROR_FAILURE; + } + } + + return NS_OK; +} + +nsresult UntrustedModulesDataSerializer::AddLoadEvents( + const UntrustedModuleLoadingEvents& aEvents, + JS::MutableHandle<JSObject*> aPerProcObj) { + JS::Rooted<JS::Value> eventsArrayVal(mCx); + if (!JS_GetProperty(mCx, aPerProcObj, "events", &eventsArrayVal) || + !eventsArrayVal.isObject()) { + return NS_ERROR_FAILURE; + } + + JS::Rooted<JSObject*> eventsArray(mCx, &eventsArrayVal.toObject()); + bool isArray; + if (!JS::IsArrayObject(mCx, eventsArray, &isArray) && !isArray) { + return NS_ERROR_FAILURE; + } + + uint32_t currentPos; + if (!GetArrayLength(mCx, eventsArray, ¤tPos)) { + return NS_ERROR_FAILURE; + } + + for (auto item : aEvents) { + JS::Rooted<JS::Value> jsel(mCx); + if (!SerializeEvent(mCx, &jsel, *item, mIndexMap) || + !JS_DefineElement(mCx, eventsArray, currentPos++, jsel, + JSPROP_ENUMERATE)) { + return NS_ERROR_FAILURE; + } + } + + return NS_OK; +} + +nsresult UntrustedModulesDataSerializer::AddSingleData( + const UntrustedModulesData& aData) { + // Serialize each entry in the modules hashtable out to the "modules" array + // and store the indices in |mIndexMap| + for (const auto& entry : aData.mModules) { + if (!mIndexMap.WithEntryHandle(entry.GetKey(), [&](auto&& addPtr) { + if (!addPtr) { + addPtr.Insert(mCurModulesArrayIdx); + + JS::Rooted<JS::Value> jsModule(mCx); + if (!SerializeModule(mCx, &jsModule, entry.GetData(), mFlags) || + !JS_DefineElement(mCx, mModulesArray, mCurModulesArrayIdx, + jsModule, JSPROP_ENUMERATE)) { + return false; + } + + ++mCurModulesArrayIdx; + } + return true; + })) { + return NS_ERROR_FAILURE; + } + } + + if (mCurModulesArrayIdx >= mMaxModulesArrayLen) { + return NS_ERROR_CANNOT_CONVERT_DATA; + } + + nsAutoCString strPid; + strPid.Append(GetProcessTypeString(aData.mProcessType)); + strPid.AppendLiteral(".0x"); + strPid.AppendInt(static_cast<uint32_t>(aData.mPid), 16); + + if (mFlags & nsITelemetry::EXCLUDE_STACKINFO_FROM_LOADEVENTS) { + JS::Rooted<JS::Value> perProcVal(mCx); + if (JS_GetProperty(mCx, mPerProcObjContainer, strPid.get(), &perProcVal) && + perProcVal.isObject()) { + // If a corresponding per-proc object already exists in the dictionary, + // and we skip to serialize CombinedStacks, we can add loading events + // into the JS object directly. + JS::Rooted<JSObject*> perProcObj(mCx, &perProcVal.toObject()); + return AddLoadEvents(aData.mEvents, &perProcObj); + } + } + + JS::Rooted<JSObject*> perProcObj(mCx, JS_NewPlainObject(mCx)); + if (!perProcObj) { + return NS_ERROR_FAILURE; + } + + nsresult rv = GetPerProcObject(aData, &perProcObj); + if (NS_FAILED(rv)) { + return rv; + } + + JS::Rooted<JS::Value> jsPerProcObjValue(mCx); + jsPerProcObjValue.setObject(*perProcObj); + if (!JS_DefineProperty(mCx, mPerProcObjContainer, strPid.get(), + jsPerProcObjValue, JSPROP_ENUMERATE)) { + return NS_ERROR_FAILURE; + } + + return NS_OK; +} + +UntrustedModulesDataSerializer::UntrustedModulesDataSerializer( + JSContext* aCx, uint32_t aMaxModulesArrayLen, uint32_t aFlags) + : mCtorResult(NS_ERROR_FAILURE), + mCx(aCx), + mMainObj(mCx, JS_NewPlainObject(mCx)), + mModulesArray(mCx, JS::NewArrayObject(mCx, 0)), + mBlockedModulesArray(mCx, JS::NewArrayObject(mCx, 0)), + mPerProcObjContainer(mCx, JS_NewPlainObject(mCx)), + mMaxModulesArrayLen(aMaxModulesArrayLen), + mCurModulesArrayIdx(0), + mCurBlockedModulesArrayIdx(0), + mFlags(aFlags) { + if (!mMainObj || !mModulesArray || !mBlockedModulesArray || + !mPerProcObjContainer) { + return; + } + + JS::Rooted<JS::Value> jsVersion(mCx); + jsVersion.setNumber(kThirdPartyModulesPingVersion); + if (!JS_DefineProperty(mCx, mMainObj, "structVersion", jsVersion, + JSPROP_ENUMERATE)) { + return; + } + + JS::Rooted<JS::Value> jsModulesArrayValue(mCx); + jsModulesArrayValue.setObject(*mModulesArray); + if (!JS_DefineProperty(mCx, mMainObj, "modules", jsModulesArrayValue, + JSPROP_ENUMERATE)) { + return; + } + + JS::Rooted<JS::Value> jsBlockedModulesArrayValue(mCx); + jsBlockedModulesArrayValue.setObject(*mBlockedModulesArray); + if (!JS_DefineProperty(mCx, mMainObj, "blockedModules", + jsBlockedModulesArrayValue, JSPROP_ENUMERATE)) { + return; + } + + JS::Rooted<JS::Value> jsPerProcObjContainerValue(mCx); + jsPerProcObjContainerValue.setObject(*mPerProcObjContainer); + if (!JS_DefineProperty(mCx, mMainObj, "processes", jsPerProcObjContainerValue, + JSPROP_ENUMERATE)) { + return; + } + + mCtorResult = NS_OK; +} + +UntrustedModulesDataSerializer::operator bool() const { + return NS_SUCCEEDED(mCtorResult); +} + +void UntrustedModulesDataSerializer::GetObject( + JS::MutableHandle<JS::Value> aRet) { + aRet.setObject(*mMainObj); +} + +nsresult UntrustedModulesDataSerializer::Add( + const UntrustedModulesBackupData& aData) { + if (NS_FAILED(mCtorResult)) { + return mCtorResult; + } + + for (const RefPtr<UntrustedModulesDataContainer>& container : + aData.Values()) { + if (!container) { + continue; + } + + nsresult rv = AddSingleData(container->mData); + if (NS_FAILED(rv)) { + return rv; + } + } + + return NS_OK; +} + +nsresult UntrustedModulesDataSerializer::AddBlockedModules( + const nsTArray<nsDependentSubstring>& blockedModules) { + if (NS_FAILED(mCtorResult)) { + return mCtorResult; + } + + if (blockedModules.Length() >= mMaxModulesArrayLen) { + return NS_ERROR_CANNOT_CONVERT_DATA; + } + + for (const auto& blockedModule : blockedModules) { + JS::Rooted<JS::Value> jsBlockedModule(mCx); + jsBlockedModule.setString(Common::ToJSString(mCx, blockedModule)); + if (!JS_DefineElement(mCx, mBlockedModulesArray, mCurBlockedModulesArrayIdx, + jsBlockedModule, JSPROP_ENUMERATE)) { + return NS_ERROR_FAILURE; + } + ++mCurBlockedModulesArrayIdx; + } + + return NS_OK; +} + +} // namespace Telemetry +} // namespace mozilla diff --git a/toolkit/components/telemetry/other/UntrustedModulesDataSerializer.h b/toolkit/components/telemetry/other/UntrustedModulesDataSerializer.h new file mode 100644 index 0000000000..cb0bce18ee --- /dev/null +++ b/toolkit/components/telemetry/other/UntrustedModulesDataSerializer.h @@ -0,0 +1,84 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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/. */ + +#ifndef UntrustedModulesDataSerializer_h__ +#define UntrustedModulesDataSerializer_h__ + +#include "js/TypeDecls.h" +#include "mozilla/UntrustedModulesData.h" +#include "mozilla/Vector.h" +#include "nsTHashMap.h" +#include "UntrustedModulesBackupService.h" + +namespace mozilla { +namespace Telemetry { + +// This class owns a JS object and serializes a given UntrustedModulesData +// into it. Because this class uses JSAPI, an AutoJSAPI instance must be +// on the stack before instantiating the class. +class MOZ_RAII UntrustedModulesDataSerializer final { + using IndexMap = nsTHashMap<nsStringHashKey, uint32_t>; + + nsresult mCtorResult; + JSContext* mCx; + JS::Rooted<JSObject*> mMainObj; + JS::Rooted<JSObject*> mModulesArray; + JS::Rooted<JSObject*> mBlockedModulesArray; + JS::Rooted<JSObject*> mPerProcObjContainer; + + IndexMap mIndexMap; + const uint32_t mMaxModulesArrayLen; + uint32_t mCurModulesArrayIdx; + uint32_t mCurBlockedModulesArrayIdx; + + // Combinations of the flags defined under nsITelemetry. + // (See "Flags for getUntrustedModuleLoadEvents" in nsITelemetry.idl) + const uint32_t mFlags; + + static bool SerializeEvent( + JSContext* aCx, JS::MutableHandle<JS::Value> aElement, + const ProcessedModuleLoadEventContainer& aEventContainer, + const IndexMap& aModuleIndices); + nsresult GetPerProcObject(const UntrustedModulesData& aData, + JS::MutableHandle<JSObject*> aObj); + nsresult AddLoadEvents(const UntrustedModuleLoadingEvents& aEvents, + JS::MutableHandle<JSObject*> aPerProcObj); + nsresult AddSingleData(const UntrustedModulesData& aData); + + public: + UntrustedModulesDataSerializer(JSContext* aCx, uint32_t aMaxModulesArrayLen, + uint32_t aFlags = 0); + explicit operator bool() const; + + /** + * Retrieves the JS object. + * + * @param aRet [out] This gets assigned to the newly created object. + */ + void GetObject(JS::MutableHandle<JS::Value> aRet); + + /** + * Adds data to the JS object. + * + * When the process of a given UntrustedModulesData collides with a key in + * the JS object, the entire UntrustedModulesData instance in the JS object + * will be replaced unless EXCLUDE_STACKINFO_FROM_LOADEVENTS is set. + * When EXCLUDE_STACKINFO_FROM_LOADEVENTS is set, the given loading events + * are appended into the JS object, keeping the existing data as is. + * + * @param aData [in] The source objects to add. + * @return nsresult + */ + nsresult Add(const UntrustedModulesBackupData& aData); + + nsresult AddBlockedModules( + const nsTArray<nsDependentSubstring>& blockedModules); +}; + +} // namespace Telemetry +} // namespace mozilla + +#endif // UntrustedModulesDataSerializer_h__ |