diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-28 14:29:10 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-28 14:29:10 +0000 |
commit | 2aa4a82499d4becd2284cdb482213d541b8804dd (patch) | |
tree | b80bf8bf13c3766139fbacc530efd0dd9d54394c /toolkit/components/telemetry/other | |
parent | Initial commit. (diff) | |
download | firefox-2aa4a82499d4becd2284cdb482213d541b8804dd.tar.xz firefox-2aa4a82499d4becd2284cdb482213d541b8804dd.zip |
Adding upstream version 86.0.1.upstream/86.0.1upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'toolkit/components/telemetry/other')
13 files changed, 2249 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..dd77328a76 --- /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 "mozilla/HangAnnotations.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::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; +} + +#if defined(MOZ_GECKO_PROFILER) +void CombinedStacks::Clear() { + mNextIndex = 0; + mStacks.clear(); + mModules.clear(); +} +#endif + +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..e28d80937e --- /dev/null +++ b/toolkit/components/telemetry/other/CombinedStacks.h @@ -0,0 +1,110 @@ +/* -*- 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); + +#if defined(MOZ_GECKO_PROFILER) + /** Clears the contents of vectors and resets the index. */ + void Clear(); +#endif + + 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(Message* aMsg, const paramType& aParam) { + WriteParam(aMsg, aParam.mModules); + WriteParam(aMsg, aParam.mStacks); + WriteParam(aMsg, aParam.mNextIndex); + WriteParam(aMsg, aParam.mMaxStacksCount); + } + + static bool Read(const Message* aMsg, PickleIterator* aIter, + paramType* aResult) { + if (!ReadParam(aMsg, aIter, &aResult->mModules)) { + return false; + } + + if (!ReadParam(aMsg, aIter, &aResult->mStacks)) { + return false; + } + + if (!ReadParam(aMsg, aIter, &aResult->mNextIndex)) { + return false; + } + + if (!ReadParam(aMsg, aIter, &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..74a77062ea --- /dev/null +++ b/toolkit/components/telemetry/other/ProcessedStack.h @@ -0,0 +1,140 @@ +/* -*- 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 <string> +#include <vector> + +#include "ipc/IPCMessageUtils.h" +#include "ipc/IPCMessageUtilsSpecializations.h" +#include "mozilla/ipc/ProtocolUtils.h" +#include "mozilla/Vector.h" +#include "nsString.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(Message* aMsg, const paramType& aParam) { + WriteParam(aMsg, aParam.mName); + WriteParam(aMsg, aParam.mBreakpadId); + } + + static bool Read(const Message* aMsg, PickleIterator* aIter, + paramType* aResult) { + if (!ReadParam(aMsg, aIter, &aResult->mName)) { + return false; + } + + if (!ReadParam(aMsg, aIter, &aResult->mBreakpadId)) { + return false; + } + + return true; + } +}; + +template <> +struct ParamTraits<mozilla::Telemetry::ProcessedStack::Frame> { + typedef mozilla::Telemetry::ProcessedStack::Frame paramType; + + static void Write(Message* aMsg, const paramType& aParam) { + WriteParam(aMsg, aParam.mOffset); + WriteParam(aMsg, aParam.mModIndex); + } + + static bool Read(const Message* aMsg, PickleIterator* aIter, + paramType* aResult) { + if (!ReadParam(aMsg, aIter, &aResult->mOffset)) { + return false; + } + + if (!ReadParam(aMsg, aIter, &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..67d3ce577c --- /dev/null +++ b/toolkit/components/telemetry/other/TelemetryIOInterposeObserver.cpp @@ -0,0 +1,179 @@ +/* -*- 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 "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::RootedObject 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..86255e5b63 --- /dev/null +++ b/toolkit/components/telemetry/other/TelemetryIOInterposeObserver.h @@ -0,0 +1,117 @@ +/* -*- 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 "jsapi.h" +#include "mozilla/IOInterposer.h" +#include "nsBaseHashtable.h" +#include "nsClassHashtable.h" +#include "nsHashKeys.h" +#include "nsTArray.h" +#include "nsTHashtable.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/UITelemetry.jsm b/toolkit/components/telemetry/other/UITelemetry.jsm new file mode 100644 index 0000000000..b14b55bb2f --- /dev/null +++ b/toolkit/components/telemetry/other/UITelemetry.jsm @@ -0,0 +1,183 @@ +/* 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/. */ + +"use strict"; + +var EXPORTED_SYMBOLS = ["UITelemetry"]; + +ChromeUtils.import("resource://gre/modules/Services.jsm", this); +ChromeUtils.import("resource://gre/modules/TelemetryUtils.jsm", this); + +/** + * UITelemetry is a helper JSM used to record UI specific telemetry events. + * + * It implements nsIUITelemetryObserver, defined in nsIAndroidBridge.idl. + */ +var UITelemetry = { + _enabled: undefined, + _activeSessions: {}, + _measurements: [], + + // Lazily decide whether telemetry is enabled. + get enabled() { + if (this._enabled !== undefined) { + return this._enabled; + } + + // Set an observer to watch for changes at runtime. + Services.prefs.addObserver( + TelemetryUtils.Preferences.TelemetryEnabled, + this + ); + Services.obs.addObserver(this, "profile-before-change"); + + // Pick up the current value. + this._enabled = Services.prefs.getBoolPref( + TelemetryUtils.Preferences.TelemetryEnabled, + false + ); + + return this._enabled; + }, + + observe(aSubject, aTopic, aData) { + if (aTopic == "profile-before-change") { + Services.obs.removeObserver(this, "profile-before-change"); + Services.prefs.removeObserver( + TelemetryUtils.Preferences.TelemetryEnabled, + this + ); + this._enabled = undefined; + return; + } + + if (aTopic == "nsPref:changed") { + switch (aData) { + case TelemetryUtils.Preferences.TelemetryEnabled: + let on = Services.prefs.getBoolPref( + TelemetryUtils.Preferences.TelemetryEnabled + ); + this._enabled = on; + + // Wipe ourselves if we were just disabled. + if (!on) { + this._activeSessions = {}; + this._measurements = []; + } + break; + } + } + }, + + /** + * This exists exclusively for testing -- our events are not intended to + * be retrieved via an XPCOM interface. + */ + get wrappedJSObject() { + return this; + }, + + /** + * A hack to generate the relative timestamp from start when we don't have + * access to the Java timer. + * XXX: Bug 1007647 - Support realtime and/or uptime in JavaScript. + */ + uptimeMillis() { + return Date.now() - Services.startup.getStartupInfo().process; + }, + + /** + * Adds a single event described by a timestamp, an action, and the calling + * method. + * + * Optionally provide a string 'extras', which will be recorded as part of + * the event. + * + * All extant sessions will be recorded by name for each event. + */ + addEvent(aAction, aMethod, aTimestamp, aExtras) { + if (!this.enabled) { + return; + } + + let sessions = Object.keys(this._activeSessions); + let aEvent = { + type: "event", + action: aAction, + method: aMethod, + sessions, + timestamp: aTimestamp == undefined ? this.uptimeMillis() : aTimestamp, + }; + + if (aExtras) { + aEvent.extras = aExtras; + } + + this._recordEvent(aEvent); + }, + + /** + * Begins tracking a session by storing a timestamp for session start. + */ + startSession(aName, aTimestamp) { + if (!this.enabled) { + return; + } + + if (this._activeSessions[aName]) { + // Do not overwrite a previous event start if it already exists. + return; + } + this._activeSessions[aName] = + aTimestamp == undefined ? this.uptimeMillis() : aTimestamp; + }, + + /** + * Tracks the end of a session with a timestamp. + */ + stopSession(aName, aReason, aTimestamp) { + if (!this.enabled) { + return; + } + + let sessionStart = this._activeSessions[aName]; + delete this._activeSessions[aName]; + + if (!sessionStart) { + return; + } + + let aEvent = { + type: "session", + name: aName, + reason: aReason, + start: sessionStart, + end: aTimestamp == undefined ? this.uptimeMillis() : aTimestamp, + }; + + this._recordEvent(aEvent); + }, + + _recordEvent(aEvent) { + this._measurements.push(aEvent); + }, + + /** + * Called by TelemetrySession to populate the UI measurement + * blob. + * + * Optionally clears the set of measurements based on aClear. + */ + getUIMeasurements(aClear) { + if (!this.enabled) { + return []; + } + + let measurements = this._measurements.slice(); + if (aClear) { + this._measurements = []; + } + return measurements; + }, +}; diff --git a/toolkit/components/telemetry/other/UntrustedModules.cpp b/toolkit/components/telemetry/other/UntrustedModules.cpp new file mode 100644 index 0000000000..db77ad8e96 --- /dev/null +++ b/toolkit/components/telemetry/other/UntrustedModules.cpp @@ -0,0 +1,259 @@ +/* -*- 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/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 { + using BackupType = UntrustedModulesBackupService::BackupType; + + 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(BackupType::Staging, 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 (RDDProcessManager* rddMgr = RDDProcessManager::Get()) { + if (RDDChild* rddChild = rddMgr->GetRDDChild()) { + AddPending(rddChild->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->Ref(BackupType::Staging)); + if (NS_WARN_IF(NS_FAILED(rv))) { + aPromise->MaybeReject(rv); + return; + } + rv = serializer.Add(mBackupSvc->Ref(BackupType::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->Ref(BackupType::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->Ref(BackupType::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(); + } + } + + JS::RootedValue 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..ad952fc336 --- /dev/null +++ b/toolkit/components/telemetry/other/UntrustedModulesBackupService.cpp @@ -0,0 +1,90 @@ +/* -*- 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) { + auto p = LookupForAdd(ProcessHashKey(aData.mProcessType, aData.mPid)); + if (p) { + p.Data()->mData.Merge(std::move(aData)); + } else { + auto data = MakeRefPtr<UntrustedModulesDataContainer>(std::move(aData)); + p.OrInsert([data = std::move(data)]() { return data; }); + } +} + +/* 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(BackupType aType, + UntrustedModulesData&& aData) { + mBackup[static_cast<uint32_t>(aType)].Add(std::move(aData)); +} + +void UntrustedModulesBackupService::SettleAllStagingData() { + UntrustedModulesBackupData staging; + staging.SwapElements(mBackup[static_cast<uint32_t>(BackupType::Staging)]); + + for (auto&& iter = staging.Iter(); !iter.Done(); iter.Next()) { + if (!iter.Data()) { + continue; + } + mBackup[static_cast<uint32_t>(BackupType::Settled)].Add( + std::move(iter.Data()->mData)); + } +} + +const UntrustedModulesBackupData& UntrustedModulesBackupService::Ref( + BackupType aType) const { + return mBackup[static_cast<uint32_t>(aType)]; +} + +} // namespace mozilla diff --git a/toolkit/components/telemetry/other/UntrustedModulesBackupService.h b/toolkit/components/telemetry/other/UntrustedModulesBackupService.h new file mode 100644 index 0000000000..15cfad216d --- /dev/null +++ b/toolkit/components/telemetry/other/UntrustedModulesBackupService.h @@ -0,0 +1,69 @@ +/* -*- 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); +}; + +class MOZ_HEAP_CLASS UntrustedModulesBackupService final { + public: + enum class BackupType : uint32_t { + Staging = 0, + Settled, + + Count + }; + + NS_INLINE_DECL_THREADSAFE_REFCOUNTING(UntrustedModulesBackupService) + + static UntrustedModulesBackupService* Get(); + void Backup(BackupType aType, UntrustedModulesData&& aData); + void SettleAllStagingData(); + const UntrustedModulesBackupData& Ref(BackupType aType) const; + + private: + UntrustedModulesBackupData mBackup[static_cast<uint32_t>(BackupType::Count)]; + + ~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..0eaa545b48 --- /dev/null +++ b/toolkit/components/telemetry/other/UntrustedModulesDataSerializer.cpp @@ -0,0 +1,548 @@ +/* -*- 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 "jsapi.h" +#include "mozilla/dom/ToJSValue.h" +#include "nsITelemetry.h" +#include "nsUnicharUtils.h" +#include "nsXULAppAPI.h" + +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::HandleObject aObj, + const char* aName, const nsAString& aVal, + size_t aMaxFieldLength = MAX_PATH) { + JS::RootedValue 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) { + uint16_t major, minor, patch, build; + + Tie(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 mozilla::Vector 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 array to convert. + * @param aElementConverter [in] A callable used to convert each array element + * to a JS element. The form of this function is: + * bool(JSContext *cx, + * JS::MutableHandleValue aRet, + * const ArrayElementT& aElement) + * @return true if aRet was successfully assigned to the new array object. + */ +template <typename T, size_t N, typename AllocPolicy, typename Converter, + typename... Args> +static bool VectorToJSArray(JSContext* cx, JS::MutableHandleObject aRet, + const Vector<T, N, AllocPolicy>& aContainer, + Converter&& aElementConverter, Args&&... aArgs) { + JS::RootedObject arr(cx, JS::NewArrayObject(cx, 0)); + if (!arr) { + return false; + } + + for (size_t i = 0, l = aContainer.length(); i < l; ++i) { + JS::RootedValue jsel(cx); + if (!aElementConverter(cx, &jsel, aContainer[i], + std::forward<Args>(aArgs)...)) { + return false; + } + if (!JS_DefineElement(cx, arr, i, jsel, JSPROP_ENUMERATE)) { + return false; + } + } + + aRet.set(arr); + return true; +} + +static bool SerializeModule(JSContext* aCx, JS::MutableHandleValue aElement, + const RefPtr<ModuleRecord>& aModule, + uint32_t aFlags) { + if (!aModule) { + return false; + } + + JS::RootedObject obj(aCx, JS_NewPlainObject(aCx)); + if (!obj) { + return false; + } + + if (aFlags & nsITelemetry::INCLUDE_PRIVATE_FIELDS_IN_LOADEVENTS) { + JS::RootedValue 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::RootedValue jsModuleVersion(aCx); + jsModuleVersion.setString( + ModuleVersionToJSString(aCx, aModule->mVersion.ref())); + if (!JS_DefineProperty(aCx, obj, "fileVersion", jsModuleVersion, + JSPROP_ENUMERATE)) { + return false; + } + } + + 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::RootedValue 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::MutableHandleValue aElement, + const ProcessedModuleLoadEvent& aEvent, const IndexMap& aModuleIndices) { + MOZ_ASSERT(NS_IsMainThread()); + + if (!aEvent) { + return false; + } + + JS::RootedObject obj(aCx, JS_NewPlainObject(aCx)); + if (!obj) { + return false; + } + + JS::RootedValue jsProcessUptimeMS(aCx); + // Javascript doesn't like 64-bit integers; convert to double. + jsProcessUptimeMS.setNumber(static_cast<double>(aEvent.mProcessUptimeMS)); + if (!JS_DefineProperty(aCx, obj, "processUptimeMS", jsProcessUptimeMS, + JSPROP_ENUMERATE)) { + return false; + } + + if (aEvent.mLoadDurationMS) { + JS::RootedValue jsLoadDurationMS(aCx); + jsLoadDurationMS.setNumber(aEvent.mLoadDurationMS.value()); + if (!JS_DefineProperty(aCx, obj, "loadDurationMS", jsLoadDurationMS, + JSPROP_ENUMERATE)) { + return false; + } + } + + JS::RootedValue jsThreadId(aCx); + jsThreadId.setNumber(static_cast<uint32_t>(aEvent.mThreadId)); + if (!JS_DefineProperty(aCx, obj, "threadID", jsThreadId, JSPROP_ENUMERATE)) { + return false; + } + + nsDependentCString effectiveThreadName; + if (aEvent.mThreadId == ::GetCurrentThreadId()) { + effectiveThreadName.Rebind("Main Thread"_ns, 0); + } else { + effectiveThreadName.Rebind(aEvent.mThreadName, 0); + } + + if (!effectiveThreadName.IsEmpty()) { + JS::RootedValue 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 (!aEvent.mRequestedDllName.IsEmpty() && + !aEvent.mRequestedDllName.Equals(aEvent.mModule->mSanitizedDllName, + nsCaseInsensitiveStringComparator)) { + if (!AddLengthLimitedStringProp(aCx, obj, "requestedDllName", + aEvent.mRequestedDllName)) { + return false; + } + } + + nsAutoString strBaseAddress; + strBaseAddress.AppendLiteral(u"0x"); + strBaseAddress.AppendInt(aEvent.mBaseAddress, 16); + + JS::RootedValue 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(aEvent.mModule->mResolvedNtName, &index)) { + return false; + } + + JS::RootedValue jsModuleIndex(aCx); + jsModuleIndex.setNumber(index); + if (!JS_DefineProperty(aCx, obj, "moduleIndex", jsModuleIndex, + JSPROP_ENUMERATE)) { + return false; + } + + JS::RootedValue jsIsDependent(aCx); + jsIsDependent.setBoolean(aEvent.mIsDependent); + if (!JS_DefineProperty(aCx, obj, "isDependent", jsIsDependent, + JSPROP_ENUMERATE)) { + return false; + } + + JS::RootedValue jsLoadStatus(aCx); + jsLoadStatus.setNumber(aEvent.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::MutableHandleObject aObj) { + JS::RootedValue jsProcType(mCx); + jsProcType.setString( + Common::ToJSString(mCx, GetProcessTypeString(aData.mProcessType))); + if (!JS_DefineProperty(mCx, aObj, "processType", jsProcType, + JSPROP_ENUMERATE)) { + return NS_ERROR_FAILURE; + } + + JS::RootedValue 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::RootedValue jsXulLoadDurationMS(mCx); + jsXulLoadDurationMS.setNumber(aData.mXULLoadDurationMS.value()); + if (!JS_DefineProperty(mCx, aObj, "xulLoadDurationMS", jsXulLoadDurationMS, + JSPROP_ENUMERATE)) { + return NS_ERROR_FAILURE; + } + } + + JS::RootedValue jsSanitizationFailures(mCx); + jsSanitizationFailures.setNumber(aData.mSanitizationFailures); + if (!JS_DefineProperty(mCx, aObj, "sanitizationFailures", + jsSanitizationFailures, JSPROP_ENUMERATE)) { + return NS_ERROR_FAILURE; + } + + JS::RootedValue jsTrustTestFailures(mCx); + jsTrustTestFailures.setNumber(aData.mTrustTestFailures); + if (!JS_DefineProperty(mCx, aObj, "trustTestFailures", jsTrustTestFailures, + JSPROP_ENUMERATE)) { + return NS_ERROR_FAILURE; + } + + JS::RootedObject eventsArray(mCx); + if (!VectorToJSArray(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::RootedObject 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 Vector<ProcessedModuleLoadEvent>& aEvents, + JS::MutableHandleObject aPerProcObj) { + JS::RootedValue eventsArrayVal(mCx); + if (!JS_GetProperty(mCx, aPerProcObj, "events", &eventsArrayVal) || + !eventsArrayVal.isObject()) { + return NS_ERROR_FAILURE; + } + + JS::RootedObject 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 (size_t i = 0, l = aEvents.length(); i < l; ++i) { + JS::RootedValue jsel(mCx); + if (!SerializeEvent(mCx, &jsel, aEvents[i], mIndexMap) || + !JS_DefineElement(mCx, eventsArray, currentPos + i, 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 (auto iter = aData.mModules.ConstIter(); !iter.Done(); iter.Next()) { + auto addPtr = mIndexMap.LookupForAdd(iter.Key()); + if (!addPtr) { + addPtr.OrInsert([idx = mCurModulesArrayIdx]() { return idx; }); + + JS::RootedValue jsModule(mCx); + if (!SerializeModule(mCx, &jsModule, iter.Data(), mFlags) || + !JS_DefineElement(mCx, mModulesArray, mCurModulesArrayIdx, jsModule, + JSPROP_ENUMERATE)) { + return NS_ERROR_FAILURE; + } + + ++mCurModulesArrayIdx; + } + } + + 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::RootedValue 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::RootedObject perProcObj(mCx, &perProcVal.toObject()); + return AddLoadEvents(aData.mEvents, &perProcObj); + } + } + + JS::RootedObject perProcObj(mCx, JS_NewPlainObject(mCx)); + if (!perProcObj) { + return NS_ERROR_FAILURE; + } + + nsresult rv = GetPerProcObject(aData, &perProcObj); + if (NS_FAILED(rv)) { + return rv; + } + + JS::RootedValue 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)), + mPerProcObjContainer(mCx, JS_NewPlainObject(mCx)), + mMaxModulesArrayLen(aMaxModulesArrayLen), + mCurModulesArrayIdx(0), + mFlags(aFlags) { + if (!mMainObj || !mModulesArray || !mPerProcObjContainer) { + return; + } + + JS::RootedValue jsVersion(mCx); + jsVersion.setNumber(kThirdPartyModulesPingVersion); + if (!JS_DefineProperty(mCx, mMainObj, "structVersion", jsVersion, + JSPROP_ENUMERATE)) { + return; + } + + JS::RootedValue jsModulesArrayValue(mCx); + jsModulesArrayValue.setObject(*mModulesArray); + if (!JS_DefineProperty(mCx, mMainObj, "modules", jsModulesArrayValue, + JSPROP_ENUMERATE)) { + return; + } + + JS::RootedValue 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::MutableHandleValue aRet) { + aRet.setObject(*mMainObj); +} + +nsresult UntrustedModulesDataSerializer::Add( + const UntrustedModulesBackupData& aData) { + if (NS_FAILED(mCtorResult)) { + return mCtorResult; + } + + for (auto iter = aData.ConstIter(); !iter.Done(); iter.Next()) { + const RefPtr<UntrustedModulesDataContainer>& container = iter.Data(); + if (!container) { + continue; + } + + nsresult rv = AddSingleData(container->mData); + if (NS_FAILED(rv)) { + return rv; + } + } + + 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..42c3a1f7fd --- /dev/null +++ b/toolkit/components/telemetry/other/UntrustedModulesDataSerializer.h @@ -0,0 +1,78 @@ +/* -*- 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 "nsDataHashtable.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 instanciating the class. +class MOZ_RAII UntrustedModulesDataSerializer final { + using IndexMap = nsDataHashtable<nsStringHashKey, uint32_t>; + + nsresult mCtorResult; + JSContext* mCx; + JS::RootedObject mMainObj; + JS::RootedObject mModulesArray; + JS::RootedObject mPerProcObjContainer; + + IndexMap mIndexMap; + const uint32_t mMaxModulesArrayLen; + uint32_t mCurModulesArrayIdx; + + // Combinations of the flags defined under nsITelemetry. + // (See "Flags for getUntrustedModuleLoadEvents" in nsITelemetry.idl) + const uint32_t mFlags; + + static bool SerializeEvent(JSContext* aCx, JS::MutableHandleValue aElement, + const ProcessedModuleLoadEvent& aEvent, + const IndexMap& aModuleIndices); + nsresult GetPerProcObject(const UntrustedModulesData& aData, + JS::MutableHandleObject aObj); + nsresult AddLoadEvents(const Vector<ProcessedModuleLoadEvent>& aEvents, + JS::MutableHandleObject 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::MutableHandleValue 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); +}; + +} // namespace Telemetry +} // namespace mozilla + +#endif // UntrustedModulesDataSerializer_h__ |