diff options
Diffstat (limited to 'toolkit/components/telemetry/Telemetry.cpp')
-rw-r--r-- | toolkit/components/telemetry/Telemetry.cpp | 3076 |
1 files changed, 3076 insertions, 0 deletions
diff --git a/toolkit/components/telemetry/Telemetry.cpp b/toolkit/components/telemetry/Telemetry.cpp new file mode 100644 index 000000000..ad2263c9b --- /dev/null +++ b/toolkit/components/telemetry/Telemetry.cpp @@ -0,0 +1,3076 @@ +/* -*- 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 <algorithm> + +#include <fstream> + +#include <prio.h> + +#include "mozilla/dom/ToJSValue.h" +#include "mozilla/Atomics.h" +#include "mozilla/Attributes.h" +#include "mozilla/DebugOnly.h" +#include "mozilla/Likely.h" +#include "mozilla/MathAlgorithms.h" +#include "mozilla/Unused.h" + +#include "base/pickle.h" +#include "nsIComponentManager.h" +#include "nsIServiceManager.h" +#include "nsThreadManager.h" +#include "nsCOMArray.h" +#include "nsCOMPtr.h" +#include "nsXPCOMPrivate.h" +#include "nsIXULAppInfo.h" +#include "nsVersionComparator.h" +#include "mozilla/MemoryReporting.h" +#include "mozilla/ModuleUtils.h" +#include "nsIXPConnect.h" +#include "mozilla/Services.h" +#include "jsapi.h" +#include "jsfriendapi.h" +#include "js/GCAPI.h" +#include "nsString.h" +#include "nsITelemetry.h" +#include "nsIFile.h" +#include "nsIFileStreams.h" +#include "nsIMemoryReporter.h" +#include "nsISeekableStream.h" +#include "Telemetry.h" +#include "TelemetryCommon.h" +#include "TelemetryHistogram.h" +#include "TelemetryScalar.h" +#include "TelemetryEvent.h" +#include "WebrtcTelemetry.h" +#include "nsTHashtable.h" +#include "nsHashKeys.h" +#include "nsBaseHashtable.h" +#include "nsClassHashtable.h" +#include "nsXULAppAPI.h" +#include "nsReadableUtils.h" +#include "nsThreadUtils.h" +#if defined(XP_WIN) +#include "nsUnicharUtils.h" +#endif +#include "nsNetCID.h" +#include "nsNetUtil.h" +#include "nsJSUtils.h" +#include "nsReadableUtils.h" +#include "plstr.h" +#include "nsAppDirectoryServiceDefs.h" +#include "mozilla/BackgroundHangMonitor.h" +#include "mozilla/ThreadHangStats.h" +#include "mozilla/ProcessedStack.h" +#include "mozilla/Mutex.h" +#include "mozilla/FileUtils.h" +#include "mozilla/Preferences.h" +#include "mozilla/StaticPtr.h" +#include "mozilla/IOInterposer.h" +#include "mozilla/PoisonIOInterposer.h" +#include "mozilla/StartupTimeline.h" +#include "mozilla/HangMonitor.h" +#if defined(MOZ_ENABLE_PROFILER_SPS) +#include "shared-libraries.h" +#endif + +namespace { + +using namespace mozilla; +using namespace mozilla::HangMonitor; +using Telemetry::Common::AutoHashtable; + +// The maximum number of chrome hangs stacks that we're keeping. +const size_t kMaxChromeStacksKept = 50; +// The maximum depth of a single chrome hang stack. +const size_t kMaxChromeStackDepth = 50; + +// 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: + CombinedStacks() : mNextIndex(0) {} + 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); + size_t GetStackCount() const; + size_t SizeOfExcludingThis() const; +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; +}; + +static JSObject * +CreateJSStackObject(JSContext *cx, const CombinedStacks &stacks); + +size_t +CombinedStacks::GetModuleCount() const { + return mModules.size(); +} + +const Telemetry::ProcessedStack::Module& +CombinedStacks::GetModule(unsigned aIndex) const { + return mModules[aIndex]; +} + +size_t +CombinedStacks::AddStack(const Telemetry::ProcessedStack& aStack) { + // Advance the indices of the circular queue holding the stacks. + size_t index = mNextIndex++ % kMaxChromeStacksKept; + // Grow the vector up to the maximum size, if needed. + if (mStacks.size() < kMaxChromeStacksKept) { + mStacks.resize(mStacks.size() + 1); + } + // Get a reference to the location holding the new stack. + CombinedStacks::Stack& adjustedStack = mStacks[index]; + // If we're using an old stack to hold aStack, clear it. + adjustedStack.clear(); + + size_t stackSize = aStack.GetStackSize(); + for (size_t i = 0; i < stackSize; ++i) { + const Telemetry::ProcessedStack::Frame& frame = aStack.GetFrame(i); + uint16_t modIndex; + if (frame.mModIndex == std::numeric_limits<uint16_t>::max()) { + modIndex = frame.mModIndex; + } else { + const Telemetry::ProcessedStack::Module& module = + aStack.GetModule(frame.mModIndex); + std::vector<Telemetry::ProcessedStack::Module>::iterator 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(); + } + } + Telemetry::ProcessedStack::Frame adjustedFrame = { frame.mOffset, modIndex }; + adjustedStack.push_back(adjustedFrame); + } + return index; +} + +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 (std::vector<Stack>::const_iterator i = mStacks.begin(), + e = mStacks.end(); i != e; ++i) { + const Stack& s = *i; + n += s.capacity() * sizeof(Telemetry::ProcessedStack::Frame); + } + return n; +} + +// This utility function generates a string key that is used to index the annotations +// in a hash map from |HangReports::AddHang|. +nsresult +ComputeAnnotationsKey(const HangAnnotationsPtr& aAnnotations, nsAString& aKeyOut) +{ + UniquePtr<HangAnnotations::Enumerator> annotationsEnum = aAnnotations->GetEnumerator(); + if (!annotationsEnum) { + return NS_ERROR_FAILURE; + } + + // Append all the attributes to the key, to uniquely identify this annotation. + nsAutoString key; + nsAutoString value; + while (annotationsEnum->Next(key, value)) { + aKeyOut.Append(key); + aKeyOut.Append(value); + } + + return NS_OK; +} + +class HangReports { +public: + /** + * This struct encapsulates information for an individual ChromeHang annotation. + * mHangIndex is the index of the corresponding ChromeHang. + */ + struct AnnotationInfo { + AnnotationInfo(uint32_t aHangIndex, + HangAnnotationsPtr aAnnotations) + : mAnnotations(Move(aAnnotations)) + { + mHangIndices.AppendElement(aHangIndex); + } + AnnotationInfo(AnnotationInfo&& aOther) + : mHangIndices(aOther.mHangIndices) + , mAnnotations(Move(aOther.mAnnotations)) + {} + ~AnnotationInfo() {} + AnnotationInfo& operator=(AnnotationInfo&& aOther) + { + mHangIndices = aOther.mHangIndices; + mAnnotations = Move(aOther.mAnnotations); + return *this; + } + // To save memory, a single AnnotationInfo can be associated to multiple chrome + // hangs. The following array holds the index of each related chrome hang. + nsTArray<uint32_t> mHangIndices; + HangAnnotationsPtr mAnnotations; + + private: + // Force move constructor + AnnotationInfo(const AnnotationInfo& aOther) = delete; + void operator=(const AnnotationInfo& aOther) = delete; + }; + size_t SizeOfExcludingThis(mozilla::MallocSizeOf aMallocSizeOf) const; + void AddHang(const Telemetry::ProcessedStack& aStack, uint32_t aDuration, + int32_t aSystemUptime, int32_t aFirefoxUptime, + HangAnnotationsPtr aAnnotations); + void PruneStackReferences(const size_t aRemovedStackIndex); + uint32_t GetDuration(unsigned aIndex) const; + int32_t GetSystemUptime(unsigned aIndex) const; + int32_t GetFirefoxUptime(unsigned aIndex) const; + const nsClassHashtable<nsStringHashKey, AnnotationInfo>& GetAnnotationInfo() const; + const CombinedStacks& GetStacks() const; +private: + /** + * This struct encapsulates the data for an individual ChromeHang, excluding + * annotations. + */ + struct HangInfo { + // Hang duration (in seconds) + uint32_t mDuration; + // System uptime (in minutes) at the time of the hang + int32_t mSystemUptime; + // Firefox uptime (in minutes) at the time of the hang + int32_t mFirefoxUptime; + }; + std::vector<HangInfo> mHangInfo; + nsClassHashtable<nsStringHashKey, AnnotationInfo> mAnnotationInfo; + CombinedStacks mStacks; +}; + +void +HangReports::AddHang(const Telemetry::ProcessedStack& aStack, + uint32_t aDuration, + int32_t aSystemUptime, + int32_t aFirefoxUptime, + HangAnnotationsPtr aAnnotations) { + // Append the new stack to the stack's circular queue. + size_t hangIndex = mStacks.AddStack(aStack); + // Append the hang info at the same index, in mHangInfo. + HangInfo info = { aDuration, aSystemUptime, aFirefoxUptime }; + if (mHangInfo.size() < kMaxChromeStacksKept) { + mHangInfo.push_back(info); + } else { + mHangInfo[hangIndex] = info; + // Remove any reference to the stack overwritten in the circular queue + // from the annotations. + PruneStackReferences(hangIndex); + } + + if (!aAnnotations) { + return; + } + + nsAutoString annotationsKey; + // Generate a key to index aAnnotations in the hash map. + nsresult rv = ComputeAnnotationsKey(aAnnotations, annotationsKey); + if (NS_FAILED(rv)) { + return; + } + + AnnotationInfo* annotationsEntry = mAnnotationInfo.Get(annotationsKey); + if (annotationsEntry) { + // If the key is already in the hash map, append the index of the chrome hang + // to its indices. + annotationsEntry->mHangIndices.AppendElement(hangIndex); + return; + } + + // If the key was not found, add the annotations to the hash map. + mAnnotationInfo.Put(annotationsKey, new AnnotationInfo(hangIndex, Move(aAnnotations))); +} + +/** + * This function removes links to discarded chrome hangs stacks and prunes unused + * annotations. + */ +void +HangReports::PruneStackReferences(const size_t aRemovedStackIndex) { + // We need to adjust the indices that link annotations to chrome hangs. Since we + // removed a stack, we must remove all references to it and prune annotations + // linked to no stacks. + for (auto iter = mAnnotationInfo.Iter(); !iter.Done(); iter.Next()) { + nsTArray<uint32_t>& stackIndices = iter.Data()->mHangIndices; + size_t toRemove = stackIndices.NoIndex; + for (size_t k = 0; k < stackIndices.Length(); k++) { + // Is this index referencing the removed stack? + if (stackIndices[k] == aRemovedStackIndex) { + toRemove = k; + break; + } + } + + // Remove the index referencing the old stack from the annotation. + if (toRemove != stackIndices.NoIndex) { + stackIndices.RemoveElementAt(toRemove); + } + + // If this annotation no longer references any stack, drop it. + if (!stackIndices.Length()) { + iter.Remove(); + } + } +} + +size_t +HangReports::SizeOfExcludingThis(mozilla::MallocSizeOf aMallocSizeOf) const { + size_t n = 0; + n += mStacks.SizeOfExcludingThis(); + // This is a crude approximation. See comment on + // CombinedStacks::SizeOfExcludingThis. + n += mHangInfo.capacity() * sizeof(HangInfo); + n += mAnnotationInfo.ShallowSizeOfExcludingThis(aMallocSizeOf); + n += mAnnotationInfo.Count() * sizeof(AnnotationInfo); + for (auto iter = mAnnotationInfo.ConstIter(); !iter.Done(); iter.Next()) { + n += iter.Key().SizeOfExcludingThisIfUnshared(aMallocSizeOf); + n += iter.Data()->mAnnotations->SizeOfIncludingThis(aMallocSizeOf); + } + return n; +} + +const CombinedStacks& +HangReports::GetStacks() const { + return mStacks; +} + +uint32_t +HangReports::GetDuration(unsigned aIndex) const { + return mHangInfo[aIndex].mDuration; +} + +int32_t +HangReports::GetSystemUptime(unsigned aIndex) const { + return mHangInfo[aIndex].mSystemUptime; +} + +int32_t +HangReports::GetFirefoxUptime(unsigned aIndex) const { + return mHangInfo[aIndex].mFirefoxUptime; +} + +const nsClassHashtable<nsStringHashKey, HangReports::AnnotationInfo>& +HangReports::GetAnnotationInfo() const { + return mAnnotationInfo; +} + +/** + * IOInterposeObserver recording statistics of main-thread I/O during execution, + * aimed at consumption by TelemetryImpl + */ +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); + + /** + * 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 { + return aMallocSizeOf(this) + SizeOfExcludingThis(aMallocSizeOf); + } + + size_t 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; + } + +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 + AutoHashtable<FileIOEntryType> mFileStats; + // Container for whitelisted 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); +}; + +TelemetryIOInterposeObserver::TelemetryIOInterposeObserver(nsIFile* aXreDir) + : mCurStage(STAGE_STARTUP) +{ + nsAutoString xreDirPath; + nsresult rv = aXreDir->GetPath(xreDirPath); + if (NS_SUCCEEDED(rv)) { + AddPath(xreDirPath, NS_LITERAL_STRING("{xre}")); + } +} + +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 + const char16_t* filename = aOb.Filename(); + + // Discard observations without filename + if (!filename) { + return; + } + +#if defined(XP_WIN) + nsCaseInsensitiveStringComparator comparator; +#else + nsDefaultStringComparator comparator; +#endif + nsAutoString processedName; + nsDependentString filenameStr(filename); + uint32_t safeDirsLen = mSafeDirs.Length(); + for (uint32_t i = 0; i < safeDirsLen; ++i) { + if (StringBeginsWith(filenameStr, mSafeDirs[i].mPath, comparator)) { + processedName = mSafeDirs[i].mSubstName; + processedName += Substring(filenameStr, 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->mData.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::AutoValueArray<NUM_STAGES> stages(cx); + + FileStatsByStage& statsByStage = entry->mData; + 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::AutoValueArray<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); +} + +// This is not a member of TelemetryImpl because we want to record I/O during +// startup. +StaticAutoPtr<TelemetryIOInterposeObserver> sTelemetryIOObserver; + +void +ClearIOReporting() +{ + if (!sTelemetryIOObserver) { + return; + } + IOInterposer::Unregister(IOInterposeObserver::OpAllWithStaging, + sTelemetryIOObserver); + sTelemetryIOObserver = nullptr; +} + +class TelemetryImpl final + : public nsITelemetry + , public nsIMemoryReporter +{ + NS_DECL_THREADSAFE_ISUPPORTS + NS_DECL_NSITELEMETRY + NS_DECL_NSIMEMORYREPORTER + +public: + void InitMemoryReporter(); + + static already_AddRefed<nsITelemetry> CreateTelemetryInstance(); + static void ShutdownTelemetry(); + static void RecordSlowStatement(const nsACString &sql, const nsACString &dbName, + uint32_t delay); +#if defined(MOZ_ENABLE_PROFILER_SPS) + static void RecordChromeHang(uint32_t aDuration, + Telemetry::ProcessedStack &aStack, + int32_t aSystemUptime, + int32_t aFirefoxUptime, + HangAnnotationsPtr aAnnotations); +#endif + static void RecordThreadHangStats(Telemetry::ThreadHangStats& aStats); + size_t SizeOfIncludingThis(mozilla::MallocSizeOf aMallocSizeOf); + struct Stat { + uint32_t hitCount; + uint32_t totalTime; + }; + struct StmtStats { + struct Stat mainThread; + struct Stat otherThreads; + }; + typedef nsBaseHashtableET<nsCStringHashKey, StmtStats> SlowSQLEntryType; + + static void RecordIceCandidates(const uint32_t iceCandidateBitmask, + const bool success); +private: + TelemetryImpl(); + ~TelemetryImpl(); + + static nsCString SanitizeSQL(const nsACString& sql); + + enum SanitizedState { Sanitized, Unsanitized }; + + static void StoreSlowSQL(const nsACString &offender, uint32_t delay, + SanitizedState state); + + static bool ReflectMainThreadSQL(SlowSQLEntryType *entry, JSContext *cx, + JS::Handle<JSObject*> obj); + static bool ReflectOtherThreadsSQL(SlowSQLEntryType *entry, JSContext *cx, + JS::Handle<JSObject*> obj); + static bool ReflectSQL(const SlowSQLEntryType *entry, const Stat *stat, + JSContext *cx, JS::Handle<JSObject*> obj); + + bool AddSQLInfo(JSContext *cx, JS::Handle<JSObject*> rootObj, bool mainThread, + bool privateSQL); + bool GetSQLStats(JSContext *cx, JS::MutableHandle<JS::Value> ret, + bool includePrivateSql); + + void ReadLateWritesStacks(nsIFile* aProfileDir); + + static TelemetryImpl *sTelemetry; + AutoHashtable<SlowSQLEntryType> mPrivateSQL; + AutoHashtable<SlowSQLEntryType> mSanitizedSQL; + Mutex mHashMutex; + HangReports mHangReports; + Mutex mHangReportsMutex; + // mThreadHangStats stores recorded, inactive thread hang stats + Vector<Telemetry::ThreadHangStats> mThreadHangStats; + Mutex mThreadHangStatsMutex; + + CombinedStacks mLateWritesStacks; // This is collected out of the main thread. + bool mCachedTelemetryData; + uint32_t mLastShutdownTime; + uint32_t mFailedLockCount; + nsCOMArray<nsIFetchTelemetryDataCallback> mCallbacks; + friend class nsFetchTelemetryData; + + WebrtcTelemetry mWebrtcTelemetry; +}; + +TelemetryImpl* TelemetryImpl::sTelemetry = nullptr; + +MOZ_DEFINE_MALLOC_SIZE_OF(TelemetryMallocSizeOf) + +NS_IMETHODIMP +TelemetryImpl::CollectReports(nsIHandleReportCallback* aHandleReport, + nsISupports* aData, bool aAnonymize) +{ + MOZ_COLLECT_REPORT( + "explicit/telemetry", KIND_HEAP, UNITS_BYTES, + SizeOfIncludingThis(TelemetryMallocSizeOf), + "Memory used by the telemetry system."); + + return NS_OK; +} + +void +InitHistogramRecordingEnabled() +{ + TelemetryHistogram::InitHistogramRecordingEnabled(); +} + +static uint32_t +ReadLastShutdownDuration(const char *filename) { + FILE *f = fopen(filename, "r"); + if (!f) { + return 0; + } + + int shutdownTime; + int r = fscanf(f, "%d\n", &shutdownTime); + fclose(f); + if (r != 1) { + return 0; + } + + return shutdownTime; +} + +const int32_t kMaxFailedProfileLockFileSize = 10; + +bool +GetFailedLockCount(nsIInputStream* inStream, uint32_t aCount, + unsigned int& result) +{ + nsAutoCString bufStr; + nsresult rv; + rv = NS_ReadInputStreamToString(inStream, bufStr, aCount); + NS_ENSURE_SUCCESS(rv, false); + result = bufStr.ToInteger(&rv); + return NS_SUCCEEDED(rv) && result > 0; +} + +nsresult +GetFailedProfileLockFile(nsIFile* *aFile, nsIFile* aProfileDir) +{ + NS_ENSURE_ARG_POINTER(aProfileDir); + + nsresult rv = aProfileDir->Clone(aFile); + NS_ENSURE_SUCCESS(rv, rv); + + (*aFile)->AppendNative(NS_LITERAL_CSTRING("Telemetry.FailedProfileLocks.txt")); + return NS_OK; +} + +class nsFetchTelemetryData : public Runnable +{ +public: + nsFetchTelemetryData(const char* aShutdownTimeFilename, + nsIFile* aFailedProfileLockFile, + nsIFile* aProfileDir) + : mShutdownTimeFilename(aShutdownTimeFilename), + mFailedProfileLockFile(aFailedProfileLockFile), + mTelemetry(TelemetryImpl::sTelemetry), + mProfileDir(aProfileDir) + { + } + +private: + const char* mShutdownTimeFilename; + nsCOMPtr<nsIFile> mFailedProfileLockFile; + RefPtr<TelemetryImpl> mTelemetry; + nsCOMPtr<nsIFile> mProfileDir; + +public: + void MainThread() { + mTelemetry->mCachedTelemetryData = true; + for (unsigned int i = 0, n = mTelemetry->mCallbacks.Count(); i < n; ++i) { + mTelemetry->mCallbacks[i]->Complete(); + } + mTelemetry->mCallbacks.Clear(); + } + + NS_IMETHOD Run() override { + LoadFailedLockCount(mTelemetry->mFailedLockCount); + mTelemetry->mLastShutdownTime = + ReadLastShutdownDuration(mShutdownTimeFilename); + mTelemetry->ReadLateWritesStacks(mProfileDir); + nsCOMPtr<nsIRunnable> e = + NewRunnableMethod(this, &nsFetchTelemetryData::MainThread); + NS_ENSURE_STATE(e); + NS_DispatchToMainThread(e); + return NS_OK; + } + +private: + nsresult + LoadFailedLockCount(uint32_t& failedLockCount) + { + failedLockCount = 0; + int64_t fileSize = 0; + nsresult rv = mFailedProfileLockFile->GetFileSize(&fileSize); + if (NS_FAILED(rv)) { + return rv; + } + NS_ENSURE_TRUE(fileSize <= kMaxFailedProfileLockFileSize, + NS_ERROR_UNEXPECTED); + nsCOMPtr<nsIInputStream> inStream; + rv = NS_NewLocalFileInputStream(getter_AddRefs(inStream), + mFailedProfileLockFile, + PR_RDONLY); + NS_ENSURE_SUCCESS(rv, rv); + NS_ENSURE_TRUE(GetFailedLockCount(inStream, fileSize, failedLockCount), + NS_ERROR_UNEXPECTED); + inStream->Close(); + + mFailedProfileLockFile->Remove(false); + return NS_OK; + } +}; + +static TimeStamp gRecordedShutdownStartTime; +static bool gAlreadyFreedShutdownTimeFileName = false; +static char *gRecordedShutdownTimeFileName = nullptr; + +static char * +GetShutdownTimeFileName() +{ + if (gAlreadyFreedShutdownTimeFileName) { + return nullptr; + } + + if (!gRecordedShutdownTimeFileName) { + nsCOMPtr<nsIFile> mozFile; + NS_GetSpecialDirectory(NS_APP_USER_PROFILE_50_DIR, getter_AddRefs(mozFile)); + if (!mozFile) + return nullptr; + + mozFile->AppendNative(NS_LITERAL_CSTRING("Telemetry.ShutdownTime.txt")); + nsAutoCString nativePath; + nsresult rv = mozFile->GetNativePath(nativePath); + if (!NS_SUCCEEDED(rv)) + return nullptr; + + gRecordedShutdownTimeFileName = PL_strdup(nativePath.get()); + } + + return gRecordedShutdownTimeFileName; +} + +NS_IMETHODIMP +TelemetryImpl::GetLastShutdownDuration(uint32_t *aResult) +{ + // The user must call AsyncFetchTelemetryData first. We return zero instead of + // reporting a failure so that the rest of telemetry can uniformly handle + // the read not being available yet. + if (!mCachedTelemetryData) { + *aResult = 0; + return NS_OK; + } + + *aResult = mLastShutdownTime; + return NS_OK; +} + +NS_IMETHODIMP +TelemetryImpl::GetFailedProfileLockCount(uint32_t* aResult) +{ + // The user must call AsyncFetchTelemetryData first. We return zero instead of + // reporting a failure so that the rest of telemetry can uniformly handle + // the read not being available yet. + if (!mCachedTelemetryData) { + *aResult = 0; + return NS_OK; + } + + *aResult = mFailedLockCount; + return NS_OK; +} + +NS_IMETHODIMP +TelemetryImpl::AsyncFetchTelemetryData(nsIFetchTelemetryDataCallback *aCallback) +{ + // We have finished reading the data already, just call the callback. + if (mCachedTelemetryData) { + aCallback->Complete(); + return NS_OK; + } + + // We already have a read request running, just remember the callback. + if (mCallbacks.Count() != 0) { + mCallbacks.AppendObject(aCallback); + return NS_OK; + } + + // We make this check so that GetShutdownTimeFileName() doesn't get + // called; calling that function without telemetry enabled violates + // assumptions that the write-the-shutdown-timestamp machinery makes. + if (!Telemetry::CanRecordExtended()) { + mCachedTelemetryData = true; + aCallback->Complete(); + return NS_OK; + } + + // Send the read to a background thread provided by the stream transport + // service to avoid a read in the main thread. + nsCOMPtr<nsIEventTarget> targetThread = + do_GetService(NS_STREAMTRANSPORTSERVICE_CONTRACTID); + if (!targetThread) { + mCachedTelemetryData = true; + aCallback->Complete(); + return NS_OK; + } + + // We have to get the filename from the main thread. + const char *shutdownTimeFilename = GetShutdownTimeFileName(); + if (!shutdownTimeFilename) { + mCachedTelemetryData = true; + aCallback->Complete(); + return NS_OK; + } + + nsCOMPtr<nsIFile> profileDir; + nsresult rv = NS_GetSpecialDirectory(NS_APP_USER_PROFILE_50_DIR, + getter_AddRefs(profileDir)); + if (NS_FAILED(rv)) { + mCachedTelemetryData = true; + aCallback->Complete(); + return NS_OK; + } + + nsCOMPtr<nsIFile> failedProfileLockFile; + rv = GetFailedProfileLockFile(getter_AddRefs(failedProfileLockFile), + profileDir); + if (NS_FAILED(rv)) { + mCachedTelemetryData = true; + aCallback->Complete(); + return NS_OK; + } + + mCallbacks.AppendObject(aCallback); + + nsCOMPtr<nsIRunnable> event = new nsFetchTelemetryData(shutdownTimeFilename, + failedProfileLockFile, + profileDir); + + targetThread->Dispatch(event, NS_DISPATCH_NORMAL); + return NS_OK; +} + +TelemetryImpl::TelemetryImpl() + : mHashMutex("Telemetry::mHashMutex") + , mHangReportsMutex("Telemetry::mHangReportsMutex") + , mThreadHangStatsMutex("Telemetry::mThreadHangStatsMutex") + , mCachedTelemetryData(false) + , mLastShutdownTime(0) + , mFailedLockCount(0) +{ + // We expect TelemetryHistogram::InitializeGlobalState() to have been + // called before we get to this point. + MOZ_ASSERT(TelemetryHistogram::GlobalStateHasBeenInitialized()); +} + +TelemetryImpl::~TelemetryImpl() { + UnregisterWeakMemoryReporter(this); +} + +void +TelemetryImpl::InitMemoryReporter() { + RegisterWeakMemoryReporter(this); +} + +bool +TelemetryImpl::ReflectSQL(const SlowSQLEntryType *entry, + const Stat *stat, + JSContext *cx, + JS::Handle<JSObject*> obj) +{ + if (stat->hitCount == 0) + return true; + + const nsACString &sql = entry->GetKey(); + + JS::Rooted<JSObject*> arrayObj(cx, JS_NewArrayObject(cx, 0)); + if (!arrayObj) { + return false; + } + return (JS_DefineElement(cx, arrayObj, 0, stat->hitCount, JSPROP_ENUMERATE) + && JS_DefineElement(cx, arrayObj, 1, stat->totalTime, JSPROP_ENUMERATE) + && JS_DefineProperty(cx, obj, sql.BeginReading(), arrayObj, + JSPROP_ENUMERATE)); +} + +bool +TelemetryImpl::ReflectMainThreadSQL(SlowSQLEntryType *entry, JSContext *cx, + JS::Handle<JSObject*> obj) +{ + return ReflectSQL(entry, &entry->mData.mainThread, cx, obj); +} + +bool +TelemetryImpl::ReflectOtherThreadsSQL(SlowSQLEntryType *entry, JSContext *cx, + JS::Handle<JSObject*> obj) +{ + return ReflectSQL(entry, &entry->mData.otherThreads, cx, obj); +} + +bool +TelemetryImpl::AddSQLInfo(JSContext *cx, JS::Handle<JSObject*> rootObj, bool mainThread, + bool privateSQL) +{ + JS::Rooted<JSObject*> statsObj(cx, JS_NewPlainObject(cx)); + if (!statsObj) + return false; + + AutoHashtable<SlowSQLEntryType>& sqlMap = (privateSQL ? mPrivateSQL : mSanitizedSQL); + AutoHashtable<SlowSQLEntryType>::ReflectEntryFunc reflectFunction = + (mainThread ? ReflectMainThreadSQL : ReflectOtherThreadsSQL); + if (!sqlMap.ReflectIntoJS(reflectFunction, cx, statsObj)) { + return false; + } + + return JS_DefineProperty(cx, rootObj, + mainThread ? "mainThread" : "otherThreads", + statsObj, JSPROP_ENUMERATE); +} + +NS_IMETHODIMP +TelemetryImpl::RegisterAddonHistogram(const nsACString &id, + const nsACString &name, + uint32_t histogramType, + uint32_t min, uint32_t max, + uint32_t bucketCount, + uint8_t optArgCount) +{ + return TelemetryHistogram::RegisterAddonHistogram + (id, name, histogramType, min, max, bucketCount, optArgCount); +} + +NS_IMETHODIMP +TelemetryImpl::GetAddonHistogram(const nsACString &id, const nsACString &name, + JSContext *cx, JS::MutableHandle<JS::Value> ret) +{ + return TelemetryHistogram::GetAddonHistogram(id, name, cx, ret); +} + +NS_IMETHODIMP +TelemetryImpl::UnregisterAddonHistograms(const nsACString &id) +{ + return TelemetryHistogram::UnregisterAddonHistograms(id); +} + +NS_IMETHODIMP +TelemetryImpl::SetHistogramRecordingEnabled(const nsACString &id, bool aEnabled) +{ + return TelemetryHistogram::SetHistogramRecordingEnabled(id, aEnabled); +} + +NS_IMETHODIMP +TelemetryImpl::GetHistogramSnapshots(JSContext *cx, JS::MutableHandle<JS::Value> ret) +{ + return TelemetryHistogram::CreateHistogramSnapshots(cx, ret, false, false); +} + +NS_IMETHODIMP +TelemetryImpl::SnapshotSubsessionHistograms(bool clearSubsession, + JSContext *cx, + JS::MutableHandle<JS::Value> ret) +{ +#if !defined(MOZ_WIDGET_GONK) && !defined(MOZ_WIDGET_ANDROID) + return TelemetryHistogram::CreateHistogramSnapshots(cx, ret, true, + clearSubsession); +#else + return NS_OK; +#endif +} + +NS_IMETHODIMP +TelemetryImpl::GetAddonHistogramSnapshots(JSContext *cx, JS::MutableHandle<JS::Value> ret) +{ + return TelemetryHistogram::GetAddonHistogramSnapshots(cx, ret); +} + +NS_IMETHODIMP +TelemetryImpl::GetKeyedHistogramSnapshots(JSContext *cx, JS::MutableHandle<JS::Value> ret) +{ + return TelemetryHistogram::GetKeyedHistogramSnapshots(cx, ret); +} + +bool +TelemetryImpl::GetSQLStats(JSContext *cx, JS::MutableHandle<JS::Value> ret, bool includePrivateSql) +{ + JS::Rooted<JSObject*> root_obj(cx, JS_NewPlainObject(cx)); + if (!root_obj) + return false; + ret.setObject(*root_obj); + + MutexAutoLock hashMutex(mHashMutex); + // Add info about slow SQL queries on the main thread + if (!AddSQLInfo(cx, root_obj, true, includePrivateSql)) + return false; + // Add info about slow SQL queries on other threads + if (!AddSQLInfo(cx, root_obj, false, includePrivateSql)) + return false; + + return true; +} + +NS_IMETHODIMP +TelemetryImpl::GetSlowSQL(JSContext *cx, JS::MutableHandle<JS::Value> ret) +{ + if (GetSQLStats(cx, ret, false)) + return NS_OK; + return NS_ERROR_FAILURE; +} + +NS_IMETHODIMP +TelemetryImpl::GetDebugSlowSQL(JSContext *cx, JS::MutableHandle<JS::Value> ret) +{ + bool revealPrivateSql = + Preferences::GetBool("toolkit.telemetry.debugSlowSql", false); + if (GetSQLStats(cx, ret, revealPrivateSql)) + return NS_OK; + return NS_ERROR_FAILURE; +} + +NS_IMETHODIMP +TelemetryImpl::GetWebrtcStats(JSContext *cx, JS::MutableHandle<JS::Value> ret) +{ + if (mWebrtcTelemetry.GetWebrtcStats(cx, ret)) + return NS_OK; + return NS_ERROR_FAILURE; +} + +NS_IMETHODIMP +TelemetryImpl::GetMaximalNumberOfConcurrentThreads(uint32_t *ret) +{ + *ret = nsThreadManager::get().GetHighestNumberOfThreads(); + return NS_OK; +} + +NS_IMETHODIMP +TelemetryImpl::GetChromeHangs(JSContext *cx, JS::MutableHandle<JS::Value> ret) +{ + MutexAutoLock hangReportMutex(mHangReportsMutex); + + const CombinedStacks& stacks = mHangReports.GetStacks(); + JS::Rooted<JSObject*> fullReportObj(cx, CreateJSStackObject(cx, stacks)); + if (!fullReportObj) { + return NS_ERROR_FAILURE; + } + + ret.setObject(*fullReportObj); + + JS::Rooted<JSObject*> durationArray(cx, JS_NewArrayObject(cx, 0)); + JS::Rooted<JSObject*> systemUptimeArray(cx, JS_NewArrayObject(cx, 0)); + JS::Rooted<JSObject*> firefoxUptimeArray(cx, JS_NewArrayObject(cx, 0)); + JS::Rooted<JSObject*> annotationsArray(cx, JS_NewArrayObject(cx, 0)); + if (!durationArray || !systemUptimeArray || !firefoxUptimeArray || + !annotationsArray) { + return NS_ERROR_FAILURE; + } + + bool ok = JS_DefineProperty(cx, fullReportObj, "durations", + durationArray, JSPROP_ENUMERATE); + if (!ok) { + return NS_ERROR_FAILURE; + } + + ok = JS_DefineProperty(cx, fullReportObj, "systemUptime", + systemUptimeArray, JSPROP_ENUMERATE); + if (!ok) { + return NS_ERROR_FAILURE; + } + + ok = JS_DefineProperty(cx, fullReportObj, "firefoxUptime", + firefoxUptimeArray, JSPROP_ENUMERATE); + if (!ok) { + return NS_ERROR_FAILURE; + } + + ok = JS_DefineProperty(cx, fullReportObj, "annotations", annotationsArray, + JSPROP_ENUMERATE); + if (!ok) { + return NS_ERROR_FAILURE; + } + + + const size_t length = stacks.GetStackCount(); + for (size_t i = 0; i < length; ++i) { + if (!JS_DefineElement(cx, durationArray, i, mHangReports.GetDuration(i), + JSPROP_ENUMERATE)) { + return NS_ERROR_FAILURE; + } + if (!JS_DefineElement(cx, systemUptimeArray, i, mHangReports.GetSystemUptime(i), + JSPROP_ENUMERATE)) { + return NS_ERROR_FAILURE; + } + if (!JS_DefineElement(cx, firefoxUptimeArray, i, mHangReports.GetFirefoxUptime(i), + JSPROP_ENUMERATE)) { + return NS_ERROR_FAILURE; + } + + size_t annotationIndex = 0; + const nsClassHashtable<nsStringHashKey, HangReports::AnnotationInfo>& annotationInfo = + mHangReports.GetAnnotationInfo(); + + for (auto iter = annotationInfo.ConstIter(); !iter.Done(); iter.Next()) { + const HangReports::AnnotationInfo* info = iter.Data(); + + JS::Rooted<JSObject*> keyValueArray(cx, JS_NewArrayObject(cx, 0)); + if (!keyValueArray) { + return NS_ERROR_FAILURE; + } + + // Create an array containing all the indices of the chrome hangs relative to this + // annotation. + JS::Rooted<JS::Value> indicesArray(cx); + if (!mozilla::dom::ToJSValue(cx, info->mHangIndices, &indicesArray)) { + return NS_ERROR_OUT_OF_MEMORY; + } + + // We're saving the annotation as [[indices], {annotation-data}], so add the indices + // array as the first element of that structure. + if (!JS_DefineElement(cx, keyValueArray, 0, indicesArray, JSPROP_ENUMERATE)) { + return NS_ERROR_FAILURE; + } + + // Create the annotations object... + JS::Rooted<JSObject*> jsAnnotation(cx, JS_NewPlainObject(cx)); + if (!jsAnnotation) { + return NS_ERROR_FAILURE; + } + UniquePtr<HangAnnotations::Enumerator> annotationsEnum = + info->mAnnotations->GetEnumerator(); + if (!annotationsEnum) { + return NS_ERROR_FAILURE; + } + + // ... fill it with key:value pairs... + nsAutoString key; + nsAutoString value; + while (annotationsEnum->Next(key, value)) { + JS::RootedValue jsValue(cx); + jsValue.setString(JS_NewUCStringCopyN(cx, value.get(), value.Length())); + if (!JS_DefineUCProperty(cx, jsAnnotation, key.get(), key.Length(), + jsValue, JSPROP_ENUMERATE)) { + return NS_ERROR_FAILURE; + } + } + + // ... and append it after the indices array. + if (!JS_DefineElement(cx, keyValueArray, 1, jsAnnotation, JSPROP_ENUMERATE)) { + return NS_ERROR_FAILURE; + } + if (!JS_DefineElement(cx, annotationsArray, annotationIndex++, + keyValueArray, JSPROP_ENUMERATE)) { + return NS_ERROR_FAILURE; + } + } + } + + return NS_OK; +} + +static 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_NewStringCopyZ(cx, module.mName.c_str())); + if (!str) { + return nullptr; + } + if (!JS_DefineElement(cx, moduleInfoArray, index++, str, JSPROP_ENUMERATE)) { + return nullptr; + } + + // Module breakpad identifier + JS::Rooted<JSString*> id(cx, JS_NewStringCopyZ(cx, module.mBreakpadId.c_str())); + if (!id) { + return nullptr; + } + if (!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; +} + +static bool +IsValidBreakpadId(const std::string &breakpadId) { + if (breakpadId.size() < 33) { + return false; + } + for (unsigned i = 0, n = breakpadId.size(); i < n; ++i) { + char c = breakpadId[i]; + if ((c < '0' || c > '9') && (c < 'A' || c > 'F')) { + return false; + } + } + return true; +} + +// Read a stack from the given file name. In case of any error, aStack is +// unchanged. +static void +ReadStack(const char *aFileName, Telemetry::ProcessedStack &aStack) +{ + std::ifstream file(aFileName); + + size_t numModules; + file >> numModules; + if (file.fail()) { + return; + } + + char newline = file.get(); + if (file.fail() || newline != '\n') { + return; + } + + Telemetry::ProcessedStack stack; + for (size_t i = 0; i < numModules; ++i) { + std::string breakpadId; + file >> breakpadId; + if (file.fail() || !IsValidBreakpadId(breakpadId)) { + return; + } + + char space = file.get(); + if (file.fail() || space != ' ') { + return; + } + + std::string moduleName; + getline(file, moduleName); + if (file.fail() || moduleName[0] == ' ') { + return; + } + + Telemetry::ProcessedStack::Module module = { + moduleName, + breakpadId + }; + stack.AddModule(module); + } + + size_t numFrames; + file >> numFrames; + if (file.fail()) { + return; + } + + newline = file.get(); + if (file.fail() || newline != '\n') { + return; + } + + for (size_t i = 0; i < numFrames; ++i) { + uint16_t index; + file >> index; + uintptr_t offset; + file >> std::hex >> offset >> std::dec; + if (file.fail()) { + return; + } + + Telemetry::ProcessedStack::Frame frame = { + offset, + index + }; + stack.AddFrame(frame); + } + + aStack = stack; +} + +static JSObject* +CreateJSTimeHistogram(JSContext* cx, const Telemetry::TimeHistogram& time) +{ + /* Create JS representation of TimeHistogram, + in the format of Chromium-style histograms. */ + JS::RootedObject ret(cx, JS_NewPlainObject(cx)); + if (!ret) { + return nullptr; + } + + if (!JS_DefineProperty(cx, ret, "min", time.GetBucketMin(0), + JSPROP_ENUMERATE) || + !JS_DefineProperty(cx, ret, "max", + time.GetBucketMax(ArrayLength(time) - 1), + JSPROP_ENUMERATE) || + !JS_DefineProperty(cx, ret, "histogram_type", + nsITelemetry::HISTOGRAM_EXPONENTIAL, + JSPROP_ENUMERATE)) { + return nullptr; + } + // TODO: calculate "sum" + if (!JS_DefineProperty(cx, ret, "sum", 0, JSPROP_ENUMERATE)) { + return nullptr; + } + + JS::RootedObject ranges( + cx, JS_NewArrayObject(cx, ArrayLength(time) + 1)); + JS::RootedObject counts( + cx, JS_NewArrayObject(cx, ArrayLength(time) + 1)); + if (!ranges || !counts) { + return nullptr; + } + /* In a Chromium-style histogram, the first bucket is an "under" bucket + that represents all values below the histogram's range. */ + if (!JS_DefineElement(cx, ranges, 0, time.GetBucketMin(0), JSPROP_ENUMERATE) || + !JS_DefineElement(cx, counts, 0, 0, JSPROP_ENUMERATE)) { + return nullptr; + } + for (size_t i = 0; i < ArrayLength(time); i++) { + if (!JS_DefineElement(cx, ranges, i + 1, time.GetBucketMax(i), + JSPROP_ENUMERATE) || + !JS_DefineElement(cx, counts, i + 1, time[i], JSPROP_ENUMERATE)) { + return nullptr; + } + } + if (!JS_DefineProperty(cx, ret, "ranges", ranges, JSPROP_ENUMERATE) || + !JS_DefineProperty(cx, ret, "counts", counts, JSPROP_ENUMERATE)) { + return nullptr; + } + return ret; +} + +static JSObject* +CreateJSHangStack(JSContext* cx, const Telemetry::HangStack& stack) +{ + JS::RootedObject ret(cx, JS_NewArrayObject(cx, stack.length())); + if (!ret) { + return nullptr; + } + for (size_t i = 0; i < stack.length(); i++) { + JS::RootedString string(cx, JS_NewStringCopyZ(cx, stack[i])); + if (!JS_DefineElement(cx, ret, i, string, JSPROP_ENUMERATE)) { + return nullptr; + } + } + return ret; +} + +static void +CreateJSHangAnnotations(JSContext* cx, const HangAnnotationsVector& annotations, + JS::MutableHandleObject returnedObject) +{ + JS::RootedObject annotationsArray(cx, JS_NewArrayObject(cx, 0)); + if (!annotationsArray) { + returnedObject.set(nullptr); + return; + } + // We keep track of the annotations we reported in this hash set, so we can + // discard duplicated ones. + nsTHashtable<nsStringHashKey> reportedAnnotations; + size_t annotationIndex = 0; + for (const HangAnnotationsPtr *i = annotations.begin(), *e = annotations.end(); + i != e; ++i) { + JS::RootedObject jsAnnotation(cx, JS_NewPlainObject(cx)); + if (!jsAnnotation) { + continue; + } + const HangAnnotationsPtr& curAnnotations = *i; + // Build a key to index the current annotations in our hash set. + nsAutoString annotationsKey; + nsresult rv = ComputeAnnotationsKey(curAnnotations, annotationsKey); + if (NS_FAILED(rv)) { + continue; + } + // Check if the annotations are in the set. If that's the case, don't double report. + if (reportedAnnotations.GetEntry(annotationsKey)) { + continue; + } + // If not, report them. + reportedAnnotations.PutEntry(annotationsKey); + UniquePtr<HangAnnotations::Enumerator> annotationsEnum = + curAnnotations->GetEnumerator(); + if (!annotationsEnum) { + continue; + } + nsAutoString key; + nsAutoString value; + while (annotationsEnum->Next(key, value)) { + JS::RootedValue jsValue(cx); + jsValue.setString(JS_NewUCStringCopyN(cx, value.get(), value.Length())); + if (!JS_DefineUCProperty(cx, jsAnnotation, key.get(), key.Length(), + jsValue, JSPROP_ENUMERATE)) { + returnedObject.set(nullptr); + return; + } + } + if (!JS_SetElement(cx, annotationsArray, annotationIndex, jsAnnotation)) { + continue; + } + ++annotationIndex; + } + // Return the array using a |MutableHandleObject| to avoid triggering a false + // positive rooting issue in the hazard analysis build. + returnedObject.set(annotationsArray); +} + +static JSObject* +CreateJSHangHistogram(JSContext* cx, const Telemetry::HangHistogram& hang) +{ + JS::RootedObject ret(cx, JS_NewPlainObject(cx)); + if (!ret) { + return nullptr; + } + + JS::RootedObject stack(cx, CreateJSHangStack(cx, hang.GetStack())); + JS::RootedObject time(cx, CreateJSTimeHistogram(cx, hang)); + auto& hangAnnotations = hang.GetAnnotations(); + JS::RootedObject annotations(cx); + CreateJSHangAnnotations(cx, hangAnnotations, &annotations); + + if (!stack || + !time || + !annotations || + !JS_DefineProperty(cx, ret, "stack", stack, JSPROP_ENUMERATE) || + !JS_DefineProperty(cx, ret, "histogram", time, JSPROP_ENUMERATE) || + (!hangAnnotations.empty() && // <-- Only define annotations when nonempty + !JS_DefineProperty(cx, ret, "annotations", annotations, JSPROP_ENUMERATE))) { + return nullptr; + } + + if (!hang.GetNativeStack().empty()) { + JS::RootedObject native(cx, CreateJSHangStack(cx, hang.GetNativeStack())); + if (!native || + !JS_DefineProperty(cx, ret, "nativeStack", native, JSPROP_ENUMERATE)) { + return nullptr; + } + } + return ret; +} + +static JSObject* +CreateJSThreadHangStats(JSContext* cx, const Telemetry::ThreadHangStats& thread) +{ + JS::RootedObject ret(cx, JS_NewPlainObject(cx)); + if (!ret) { + return nullptr; + } + JS::RootedString name(cx, JS_NewStringCopyZ(cx, thread.GetName())); + if (!name || + !JS_DefineProperty(cx, ret, "name", name, JSPROP_ENUMERATE)) { + return nullptr; + } + + JS::RootedObject activity(cx, CreateJSTimeHistogram(cx, thread.mActivity)); + if (!activity || + !JS_DefineProperty(cx, ret, "activity", activity, JSPROP_ENUMERATE)) { + return nullptr; + } + + JS::RootedObject hangs(cx, JS_NewArrayObject(cx, 0)); + if (!hangs) { + return nullptr; + } + for (size_t i = 0; i < thread.mHangs.length(); i++) { + JS::RootedObject obj(cx, CreateJSHangHistogram(cx, thread.mHangs[i])); + if (!JS_DefineElement(cx, hangs, i, obj, JSPROP_ENUMERATE)) { + return nullptr; + } + } + if (!JS_DefineProperty(cx, ret, "hangs", hangs, JSPROP_ENUMERATE)) { + return nullptr; + } + + return ret; +} + +NS_IMETHODIMP +TelemetryImpl::GetThreadHangStats(JSContext* cx, JS::MutableHandle<JS::Value> ret) +{ + JS::RootedObject retObj(cx, JS_NewArrayObject(cx, 0)); + if (!retObj) { + return NS_ERROR_FAILURE; + } + size_t threadIndex = 0; + + if (!BackgroundHangMonitor::IsDisabled()) { + /* First add active threads; we need to hold |iter| (and its lock) + throughout this method to avoid a race condition where a thread can + be recorded twice if the thread is destroyed while this method is + running */ + BackgroundHangMonitor::ThreadHangStatsIterator iter; + for (Telemetry::ThreadHangStats* histogram = iter.GetNext(); + histogram; histogram = iter.GetNext()) { + JS::RootedObject obj(cx, CreateJSThreadHangStats(cx, *histogram)); + if (!JS_DefineElement(cx, retObj, threadIndex++, obj, JSPROP_ENUMERATE)) { + return NS_ERROR_FAILURE; + } + } + } + + // Add saved threads next + MutexAutoLock autoLock(mThreadHangStatsMutex); + for (size_t i = 0; i < mThreadHangStats.length(); i++) { + JS::RootedObject obj(cx, + CreateJSThreadHangStats(cx, mThreadHangStats[i])); + if (!JS_DefineElement(cx, retObj, threadIndex++, obj, JSPROP_ENUMERATE)) { + return NS_ERROR_FAILURE; + } + } + ret.setObject(*retObj); + return NS_OK; +} + +void +TelemetryImpl::ReadLateWritesStacks(nsIFile* aProfileDir) +{ + nsAutoCString nativePath; + nsresult rv = aProfileDir->GetNativePath(nativePath); + if (NS_FAILED(rv)) { + return; + } + + const char *name = nativePath.get(); + PRDir *dir = PR_OpenDir(name); + if (!dir) { + return; + } + + PRDirEntry *ent; + const char *prefix = "Telemetry.LateWriteFinal-"; + unsigned int prefixLen = strlen(prefix); + while ((ent = PR_ReadDir(dir, PR_SKIP_NONE))) { + if (strncmp(prefix, ent->name, prefixLen) != 0) { + continue; + } + + nsAutoCString stackNativePath = nativePath; + stackNativePath += XPCOM_FILE_PATH_SEPARATOR; + stackNativePath += nsDependentCString(ent->name); + + Telemetry::ProcessedStack stack; + ReadStack(stackNativePath.get(), stack); + if (stack.GetStackSize() != 0) { + mLateWritesStacks.AddStack(stack); + } + // Delete the file so that we don't report it again on the next run. + PR_Delete(stackNativePath.get()); + } + PR_CloseDir(dir); +} + +NS_IMETHODIMP +TelemetryImpl::GetLateWrites(JSContext *cx, JS::MutableHandle<JS::Value> ret) +{ + // The user must call AsyncReadTelemetryData first. We return an empty list + // instead of reporting a failure so that the rest of telemetry can uniformly + // handle the read not being available yet. + + // FIXME: we allocate the js object again and again in the getter. We should + // figure out a way to cache it. In order to do that we have to call + // JS_AddNamedObjectRoot. A natural place to do so is in the TelemetryImpl + // constructor, but it is not clear how to get a JSContext in there. + // Another option would be to call it in here when we first call + // CreateJSStackObject, but we would still need to figure out where to call + // JS_RemoveObjectRoot. Would it be ok to never call JS_RemoveObjectRoot + // and just set the pointer to nullptr is the telemetry destructor? + + JSObject *report; + if (!mCachedTelemetryData) { + CombinedStacks empty; + report = CreateJSStackObject(cx, empty); + } else { + report = CreateJSStackObject(cx, mLateWritesStacks); + } + + if (report == nullptr) { + return NS_ERROR_FAILURE; + } + + ret.setObject(*report); + return NS_OK; +} + +NS_IMETHODIMP +TelemetryImpl::RegisteredHistograms(uint32_t aDataset, uint32_t *aCount, + char*** aHistograms) +{ + return + TelemetryHistogram::RegisteredHistograms(aDataset, aCount, aHistograms); +} + +NS_IMETHODIMP +TelemetryImpl::RegisteredKeyedHistograms(uint32_t aDataset, uint32_t *aCount, + char*** aHistograms) +{ + return + TelemetryHistogram::RegisteredKeyedHistograms(aDataset, aCount, + aHistograms); +} + +NS_IMETHODIMP +TelemetryImpl::GetHistogramById(const nsACString &name, JSContext *cx, + JS::MutableHandle<JS::Value> ret) +{ + return TelemetryHistogram::GetHistogramById(name, cx, ret); +} + +NS_IMETHODIMP +TelemetryImpl::GetKeyedHistogramById(const nsACString &name, JSContext *cx, + JS::MutableHandle<JS::Value> ret) +{ + return TelemetryHistogram::GetKeyedHistogramById(name, cx, ret); +} + +/** + * Indicates if Telemetry can record base data (FHR data). This is true if the + * FHR data reporting service or self-support are enabled. + * + * In the unlikely event that adding a new base probe is needed, please check the data + * collection wiki at https://wiki.mozilla.org/Firefox/Data_Collection and talk to the + * Telemetry team. + */ +NS_IMETHODIMP +TelemetryImpl::GetCanRecordBase(bool *ret) { + *ret = TelemetryHistogram::CanRecordBase(); + return NS_OK; +} + +NS_IMETHODIMP +TelemetryImpl::SetCanRecordBase(bool canRecord) { + TelemetryHistogram::SetCanRecordBase(canRecord); + TelemetryScalar::SetCanRecordBase(canRecord); + TelemetryEvent::SetCanRecordBase(canRecord); + return NS_OK; +} + +/** + * Indicates if Telemetry is allowed to record extended data. Returns false if the user + * hasn't opted into "extended Telemetry" on the Release channel, when the user has + * explicitly opted out of Telemetry on Nightly/Aurora/Beta or if manually set to false + * during tests. + * If the returned value is false, gathering of extended telemetry statistics is disabled. + */ +NS_IMETHODIMP +TelemetryImpl::GetCanRecordExtended(bool *ret) { + *ret = TelemetryHistogram::CanRecordExtended(); + return NS_OK; +} + +NS_IMETHODIMP +TelemetryImpl::SetCanRecordExtended(bool canRecord) { + TelemetryHistogram::SetCanRecordExtended(canRecord); + TelemetryScalar::SetCanRecordExtended(canRecord); + TelemetryEvent::SetCanRecordExtended(canRecord); + return NS_OK; +} + + +NS_IMETHODIMP +TelemetryImpl::GetIsOfficialTelemetry(bool *ret) { +#if defined(MOZILLA_OFFICIAL) && defined(MOZ_TELEMETRY_REPORTING) && !defined(DEBUG) + *ret = true; +#else + *ret = false; +#endif + return NS_OK; +} + +already_AddRefed<nsITelemetry> +TelemetryImpl::CreateTelemetryInstance() +{ + MOZ_ASSERT(sTelemetry == nullptr, "CreateTelemetryInstance may only be called once, via GetService()"); + + bool useTelemetry = false; + if (XRE_IsParentProcess() || + XRE_IsContentProcess() || + XRE_IsGPUProcess()) + { + useTelemetry = true; + } + + // First, initialize the TelemetryHistogram and TelemetryScalar global states. + TelemetryHistogram::InitializeGlobalState(useTelemetry, useTelemetry); + + // Only record scalars from the parent process. + TelemetryScalar::InitializeGlobalState(XRE_IsParentProcess(), XRE_IsParentProcess()); + + // Only record events from the parent process. + TelemetryEvent::InitializeGlobalState(XRE_IsParentProcess(), XRE_IsParentProcess()); + + // Now, create and initialize the Telemetry global state. + sTelemetry = new TelemetryImpl(); + + // AddRef for the local reference + NS_ADDREF(sTelemetry); + // AddRef for the caller + nsCOMPtr<nsITelemetry> ret = sTelemetry; + + sTelemetry->InitMemoryReporter(); + InitHistogramRecordingEnabled(); // requires sTelemetry to exist + + return ret.forget(); +} + +void +TelemetryImpl::ShutdownTelemetry() +{ + // No point in collecting IO beyond this point + ClearIOReporting(); + NS_IF_RELEASE(sTelemetry); + + // Lastly, de-initialise the TelemetryHistogram and TelemetryScalar global states, + // so as to release any heap storage that would otherwise be kept alive by it. + TelemetryHistogram::DeInitializeGlobalState(); + TelemetryScalar::DeInitializeGlobalState(); + TelemetryEvent::DeInitializeGlobalState(); +} + +void +TelemetryImpl::StoreSlowSQL(const nsACString &sql, uint32_t delay, + SanitizedState state) +{ + AutoHashtable<SlowSQLEntryType>* slowSQLMap = nullptr; + if (state == Sanitized) + slowSQLMap = &(sTelemetry->mSanitizedSQL); + else + slowSQLMap = &(sTelemetry->mPrivateSQL); + + MutexAutoLock hashMutex(sTelemetry->mHashMutex); + + SlowSQLEntryType *entry = slowSQLMap->GetEntry(sql); + if (!entry) { + entry = slowSQLMap->PutEntry(sql); + if (MOZ_UNLIKELY(!entry)) + return; + entry->mData.mainThread.hitCount = 0; + entry->mData.mainThread.totalTime = 0; + entry->mData.otherThreads.hitCount = 0; + entry->mData.otherThreads.totalTime = 0; + } + + if (NS_IsMainThread()) { + entry->mData.mainThread.hitCount++; + entry->mData.mainThread.totalTime += delay; + } else { + entry->mData.otherThreads.hitCount++; + entry->mData.otherThreads.totalTime += delay; + } +} + +/** + * This method replaces string literals in SQL strings with the word :private + * + * States used in this state machine: + * + * NORMAL: + * - This is the active state when not iterating over a string literal or + * comment + * + * SINGLE_QUOTE: + * - Defined here: http://www.sqlite.org/lang_expr.html + * - This state represents iterating over a string literal opened with + * a single quote. + * - A single quote within the string can be encoded by putting 2 single quotes + * in a row, e.g. 'This literal contains an escaped quote ''' + * - Any double quotes found within a single-quoted literal are ignored + * - This state covers BLOB literals, e.g. X'ABC123' + * - The string literal and the enclosing quotes will be replaced with + * the text :private + * + * DOUBLE_QUOTE: + * - Same rules as the SINGLE_QUOTE state. + * - According to http://www.sqlite.org/lang_keywords.html, + * SQLite interprets text in double quotes as an identifier unless it's used in + * a context where it cannot be resolved to an identifier and a string literal + * is allowed. This method removes text in double-quotes for safety. + * + * DASH_COMMENT: + * - http://www.sqlite.org/lang_comment.html + * - A dash comment starts with two dashes in a row, + * e.g. DROP TABLE foo -- a comment + * - Any text following two dashes in a row is interpreted as a comment until + * end of input or a newline character + * - Any quotes found within the comment are ignored and no replacements made + * + * C_STYLE_COMMENT: + * - http://www.sqlite.org/lang_comment.html + * - A C-style comment starts with a forward slash and an asterisk, and ends + * with an asterisk and a forward slash + * - Any text following comment start is interpreted as a comment up to end of + * input or comment end + * - Any quotes found within the comment are ignored and no replacements made + */ +nsCString +TelemetryImpl::SanitizeSQL(const nsACString &sql) { + nsCString output; + int length = sql.Length(); + + typedef enum { + NORMAL, + SINGLE_QUOTE, + DOUBLE_QUOTE, + DASH_COMMENT, + C_STYLE_COMMENT, + } State; + + State state = NORMAL; + int fragmentStart = 0; + for (int i = 0; i < length; i++) { + char character = sql[i]; + char nextCharacter = (i + 1 < length) ? sql[i + 1] : '\0'; + + switch (character) { + case '\'': + case '"': + if (state == NORMAL) { + state = (character == '\'') ? SINGLE_QUOTE : DOUBLE_QUOTE; + output += nsDependentCSubstring(sql, fragmentStart, i - fragmentStart); + output += ":private"; + fragmentStart = -1; + } else if ((state == SINGLE_QUOTE && character == '\'') || + (state == DOUBLE_QUOTE && character == '"')) { + if (nextCharacter == character) { + // Two consecutive quotes within a string literal are a single escaped quote + i++; + } else { + state = NORMAL; + fragmentStart = i + 1; + } + } + break; + case '-': + if (state == NORMAL) { + if (nextCharacter == '-') { + state = DASH_COMMENT; + i++; + } + } + break; + case '\n': + if (state == DASH_COMMENT) { + state = NORMAL; + } + break; + case '/': + if (state == NORMAL) { + if (nextCharacter == '*') { + state = C_STYLE_COMMENT; + i++; + } + } + break; + case '*': + if (state == C_STYLE_COMMENT) { + if (nextCharacter == '/') { + state = NORMAL; + } + } + break; + default: + continue; + } + } + + if ((fragmentStart >= 0) && fragmentStart < length) + output += nsDependentCSubstring(sql, fragmentStart, length - fragmentStart); + + return output; +} + +// A whitelist mechanism to prevent Telemetry reporting on Addon & Thunderbird +// DBs. +struct TrackedDBEntry +{ + const char* mName; + const uint32_t mNameLength; + + // This struct isn't meant to be used beyond the static arrays below. + constexpr + TrackedDBEntry(const char* aName, uint32_t aNameLength) + : mName(aName) + , mNameLength(aNameLength) + { } + + TrackedDBEntry() = delete; + TrackedDBEntry(TrackedDBEntry&) = delete; +}; + +#define TRACKEDDB_ENTRY(_name) { _name, (sizeof(_name) - 1) } + +// A whitelist of database names. If the database name exactly matches one of +// these then its SQL statements will always be recorded. +static constexpr TrackedDBEntry kTrackedDBs[] = { + // IndexedDB for about:home, see aboutHome.js + TRACKEDDB_ENTRY("818200132aebmoouht.sqlite"), + TRACKEDDB_ENTRY("addons.sqlite"), + TRACKEDDB_ENTRY("content-prefs.sqlite"), + TRACKEDDB_ENTRY("cookies.sqlite"), + TRACKEDDB_ENTRY("downloads.sqlite"), + TRACKEDDB_ENTRY("extensions.sqlite"), + TRACKEDDB_ENTRY("formhistory.sqlite"), + TRACKEDDB_ENTRY("index.sqlite"), + TRACKEDDB_ENTRY("netpredictions.sqlite"), + TRACKEDDB_ENTRY("permissions.sqlite"), + TRACKEDDB_ENTRY("places.sqlite"), + TRACKEDDB_ENTRY("reading-list.sqlite"), + TRACKEDDB_ENTRY("search.sqlite"), + TRACKEDDB_ENTRY("signons.sqlite"), + TRACKEDDB_ENTRY("urlclassifier3.sqlite"), + TRACKEDDB_ENTRY("webappsstore.sqlite") +}; + +// A whitelist of database name prefixes. If the database name begins with +// one of these prefixes then its SQL statements will always be recorded. +static const TrackedDBEntry kTrackedDBPrefixes[] = { + TRACKEDDB_ENTRY("indexedDB-") +}; + +#undef TRACKEDDB_ENTRY + +// Slow SQL statements will be automatically +// trimmed to kMaxSlowStatementLength characters. +// This limit doesn't include the ellipsis and DB name, +// that are appended at the end of the stored statement. +const uint32_t kMaxSlowStatementLength = 1000; + +void +TelemetryImpl::RecordSlowStatement(const nsACString &sql, + const nsACString &dbName, + uint32_t delay) +{ + MOZ_ASSERT(!sql.IsEmpty()); + MOZ_ASSERT(!dbName.IsEmpty()); + + if (!sTelemetry || !TelemetryHistogram::CanRecordExtended()) + return; + + bool recordStatement = false; + + for (const TrackedDBEntry& nameEntry : kTrackedDBs) { + MOZ_ASSERT(nameEntry.mNameLength); + const nsDependentCString name(nameEntry.mName, nameEntry.mNameLength); + if (dbName == name) { + recordStatement = true; + break; + } + } + + if (!recordStatement) { + for (const TrackedDBEntry& prefixEntry : kTrackedDBPrefixes) { + MOZ_ASSERT(prefixEntry.mNameLength); + const nsDependentCString prefix(prefixEntry.mName, + prefixEntry.mNameLength); + if (StringBeginsWith(dbName, prefix)) { + recordStatement = true; + break; + } + } + } + + if (recordStatement) { + nsAutoCString sanitizedSQL(SanitizeSQL(sql)); + if (sanitizedSQL.Length() > kMaxSlowStatementLength) { + sanitizedSQL.SetLength(kMaxSlowStatementLength); + sanitizedSQL += "..."; + } + sanitizedSQL.AppendPrintf(" /* %s */", nsPromiseFlatCString(dbName).get()); + StoreSlowSQL(sanitizedSQL, delay, Sanitized); + } else { + // Report aggregate DB-level statistics for addon DBs + nsAutoCString aggregate; + aggregate.AppendPrintf("Untracked SQL for %s", + nsPromiseFlatCString(dbName).get()); + StoreSlowSQL(aggregate, delay, Sanitized); + } + + nsAutoCString fullSQL; + fullSQL.AppendPrintf("%s /* %s */", + nsPromiseFlatCString(sql).get(), + nsPromiseFlatCString(dbName).get()); + StoreSlowSQL(fullSQL, delay, Unsanitized); +} + +void +TelemetryImpl::RecordIceCandidates(const uint32_t iceCandidateBitmask, + const bool success) +{ + if (!sTelemetry || !TelemetryHistogram::CanRecordExtended()) + return; + + sTelemetry->mWebrtcTelemetry.RecordIceCandidateMask(iceCandidateBitmask, success); +} + +#if defined(MOZ_ENABLE_PROFILER_SPS) +void +TelemetryImpl::RecordChromeHang(uint32_t aDuration, + Telemetry::ProcessedStack &aStack, + int32_t aSystemUptime, + int32_t aFirefoxUptime, + HangAnnotationsPtr aAnnotations) +{ + if (!sTelemetry || !TelemetryHistogram::CanRecordExtended()) + return; + + HangAnnotationsPtr annotations; + // We only pass aAnnotations if it is not empty. + if (aAnnotations && !aAnnotations->IsEmpty()) { + annotations = Move(aAnnotations); + } + + MutexAutoLock hangReportMutex(sTelemetry->mHangReportsMutex); + + sTelemetry->mHangReports.AddHang(aStack, aDuration, + aSystemUptime, aFirefoxUptime, + Move(annotations)); +} +#endif + +void +TelemetryImpl::RecordThreadHangStats(Telemetry::ThreadHangStats& aStats) +{ + if (!sTelemetry || !TelemetryHistogram::CanRecordExtended()) + return; + + MutexAutoLock autoLock(sTelemetry->mThreadHangStatsMutex); + + // Ignore OOM. + mozilla::Unused << sTelemetry->mThreadHangStats.append(Move(aStats)); +} + +NS_IMPL_ISUPPORTS(TelemetryImpl, nsITelemetry, nsIMemoryReporter) +NS_GENERIC_FACTORY_SINGLETON_CONSTRUCTOR(nsITelemetry, TelemetryImpl::CreateTelemetryInstance) + +#define NS_TELEMETRY_CID \ + {0xaea477f2, 0xb3a2, 0x469c, {0xaa, 0x29, 0x0a, 0x82, 0xd1, 0x32, 0xb8, 0x29}} +NS_DEFINE_NAMED_CID(NS_TELEMETRY_CID); + +const Module::CIDEntry kTelemetryCIDs[] = { + { &kNS_TELEMETRY_CID, false, nullptr, nsITelemetryConstructor, Module::ALLOW_IN_GPU_PROCESS }, + { nullptr } +}; + +const Module::ContractIDEntry kTelemetryContracts[] = { + { "@mozilla.org/base/telemetry;1", &kNS_TELEMETRY_CID, Module::ALLOW_IN_GPU_PROCESS }, + { nullptr } +}; + +const Module kTelemetryModule = { + Module::kVersion, + kTelemetryCIDs, + kTelemetryContracts, + nullptr, + nullptr, + nullptr, + TelemetryImpl::ShutdownTelemetry, + Module::ALLOW_IN_GPU_PROCESS +}; + +NS_IMETHODIMP +TelemetryImpl::GetFileIOReports(JSContext *cx, JS::MutableHandleValue ret) +{ + if (sTelemetryIOObserver) { + JS::Rooted<JSObject*> obj(cx, JS_NewPlainObject(cx)); + if (!obj) { + return NS_ERROR_FAILURE; + } + + if (!sTelemetryIOObserver->ReflectIntoJS(cx, obj)) { + return NS_ERROR_FAILURE; + } + ret.setObject(*obj); + return NS_OK; + } + ret.setNull(); + return NS_OK; +} + +NS_IMETHODIMP +TelemetryImpl::MsSinceProcessStart(double* aResult) +{ + return Telemetry::Common::MsSinceProcessStart(aResult); +} + +// Telemetry Scalars IDL Implementation + +NS_IMETHODIMP +TelemetryImpl::ScalarAdd(const nsACString& aName, JS::HandleValue aVal, JSContext* aCx) +{ + return TelemetryScalar::Add(aName, aVal, aCx); +} + +NS_IMETHODIMP +TelemetryImpl::ScalarSet(const nsACString& aName, JS::HandleValue aVal, JSContext* aCx) +{ + return TelemetryScalar::Set(aName, aVal, aCx); +} + +NS_IMETHODIMP +TelemetryImpl::ScalarSetMaximum(const nsACString& aName, JS::HandleValue aVal, JSContext* aCx) +{ + return TelemetryScalar::SetMaximum(aName, aVal, aCx); +} + +NS_IMETHODIMP +TelemetryImpl::SnapshotScalars(unsigned int aDataset, bool aClearScalars, JSContext* aCx, + uint8_t optional_argc, JS::MutableHandleValue aResult) +{ + return TelemetryScalar::CreateSnapshots(aDataset, aClearScalars, aCx, optional_argc, aResult); +} + +NS_IMETHODIMP +TelemetryImpl::KeyedScalarAdd(const nsACString& aName, const nsAString& aKey, + JS::HandleValue aVal, JSContext* aCx) +{ + return TelemetryScalar::Add(aName, aKey, aVal, aCx); +} + +NS_IMETHODIMP +TelemetryImpl::KeyedScalarSet(const nsACString& aName, const nsAString& aKey, + JS::HandleValue aVal, JSContext* aCx) +{ + return TelemetryScalar::Set(aName, aKey, aVal, aCx); +} + +NS_IMETHODIMP +TelemetryImpl::KeyedScalarSetMaximum(const nsACString& aName, const nsAString& aKey, + JS::HandleValue aVal, JSContext* aCx) +{ + return TelemetryScalar::SetMaximum(aName, aKey, aVal, aCx); +} + +NS_IMETHODIMP +TelemetryImpl::SnapshotKeyedScalars(unsigned int aDataset, bool aClearScalars, JSContext* aCx, + uint8_t optional_argc, JS::MutableHandleValue aResult) +{ + return TelemetryScalar::CreateKeyedSnapshots(aDataset, aClearScalars, aCx, optional_argc, + aResult); +} + +NS_IMETHODIMP +TelemetryImpl::ClearScalars() +{ + TelemetryScalar::ClearScalars(); + return NS_OK; +} + +// Telemetry Event IDL implementation. + +NS_IMETHODIMP +TelemetryImpl::RecordEvent(const nsACString & aCategory, const nsACString & aMethod, + const nsACString & aObject, JS::HandleValue aValue, + JS::HandleValue aExtra, JSContext* aCx, uint8_t optional_argc) +{ + return TelemetryEvent::RecordEvent(aCategory, aMethod, aObject, aValue, aExtra, aCx, optional_argc); +} + +NS_IMETHODIMP +TelemetryImpl::SnapshotBuiltinEvents(uint32_t aDataset, bool aClear, JSContext* aCx, + uint8_t optional_argc, JS::MutableHandleValue aResult) +{ + return TelemetryEvent::CreateSnapshots(aDataset, aClear, aCx, optional_argc, aResult); +} + +NS_IMETHODIMP +TelemetryImpl::ClearEvents() +{ + TelemetryEvent::ClearEvents(); + return NS_OK; +} + + +NS_IMETHODIMP +TelemetryImpl::FlushBatchedChildTelemetry() +{ + TelemetryHistogram::IPCTimerFired(nullptr, nullptr); + return NS_OK; +} + +size_t +TelemetryImpl::SizeOfIncludingThis(mozilla::MallocSizeOf aMallocSizeOf) +{ + size_t n = aMallocSizeOf(this); + + // Ignore the hashtables in mAddonMap; they are not significant. + n += TelemetryHistogram::GetMapShallowSizesOfExcludingThis(aMallocSizeOf); + n += TelemetryScalar::GetMapShallowSizesOfExcludingThis(aMallocSizeOf); + n += mWebrtcTelemetry.SizeOfExcludingThis(aMallocSizeOf); + { // Scope for mHashMutex lock + MutexAutoLock lock(mHashMutex); + n += mPrivateSQL.SizeOfExcludingThis(aMallocSizeOf); + n += mSanitizedSQL.SizeOfExcludingThis(aMallocSizeOf); + } + { // Scope for mHangReportsMutex lock + MutexAutoLock lock(mHangReportsMutex); + n += mHangReports.SizeOfExcludingThis(aMallocSizeOf); + } + { // Scope for mThreadHangStatsMutex lock + MutexAutoLock lock(mThreadHangStatsMutex); + n += mThreadHangStats.sizeOfExcludingThis(aMallocSizeOf); + } + + // It's a bit gross that we measure this other stuff that lives outside of + // TelemetryImpl... oh well. + if (sTelemetryIOObserver) { + n += sTelemetryIOObserver->SizeOfIncludingThis(aMallocSizeOf); + } + + n += TelemetryHistogram::GetHistogramSizesofIncludingThis(aMallocSizeOf); + n += TelemetryScalar::GetScalarSizesOfIncludingThis(aMallocSizeOf); + n += TelemetryEvent::SizeOfIncludingThis(aMallocSizeOf); + + return n; +} + +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_ENABLE_PROFILER_SPS +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 + + +//////////////////////////////////////////////////////////////////////// +//////////////////////////////////////////////////////////////////////// +// +// EXTERNALLY VISIBLE FUNCTIONS in no name space +// These are NOT listed in Telemetry.h + +NSMODULE_DEFN(nsTelemetryModule) = &kTelemetryModule; + +/** + * The XRE_TelemetryAdd function is to be used by embedding applications + * that can't use mozilla::Telemetry::Accumulate() directly. + */ +void +XRE_TelemetryAccumulate(int aID, uint32_t aSample) +{ + mozilla::Telemetry::Accumulate((mozilla::Telemetry::ID) aID, aSample); +} + + +//////////////////////////////////////////////////////////////////////// +//////////////////////////////////////////////////////////////////////// +// +// EXTERNALLY VISIBLE FUNCTIONS in mozilla:: +// These are NOT listed in Telemetry.h + +namespace mozilla { + +void +RecordShutdownStartTimeStamp() { +#ifdef DEBUG + // FIXME: this function should only be called once, since it should be called + // at the earliest point we *know* we are shutting down. Unfortunately + // this assert has been firing. Given that if we are called multiple times + // we just keep the last timestamp, the assert is commented for now. + static bool recorded = false; + // MOZ_ASSERT(!recorded); + (void)recorded; // Silence unused-var warnings (remove when assert re-enabled) + recorded = true; +#endif + + if (!Telemetry::CanRecordExtended()) + return; + + gRecordedShutdownStartTime = TimeStamp::Now(); + + GetShutdownTimeFileName(); +} + +void +RecordShutdownEndTimeStamp() { + if (!gRecordedShutdownTimeFileName || gAlreadyFreedShutdownTimeFileName) + return; + + nsCString name(gRecordedShutdownTimeFileName); + PL_strfree(gRecordedShutdownTimeFileName); + gRecordedShutdownTimeFileName = nullptr; + gAlreadyFreedShutdownTimeFileName = true; + + if (gRecordedShutdownStartTime.IsNull()) { + // If |CanRecordExtended()| is true before |AsyncFetchTelemetryData| is called and + // then disabled before shutdown, |RecordShutdownStartTimeStamp| will bail out and + // we will end up with a null |gRecordedShutdownStartTime| here. This can happen + // during tests. + return; + } + + nsCString tmpName = name; + tmpName += ".tmp"; + FILE *f = fopen(tmpName.get(), "w"); + if (!f) + return; + // On a normal release build this should be called just before + // calling _exit, but on a debug build or when the user forces a full + // shutdown this is called as late as possible, so we have to + // white list this write as write poisoning will be enabled. + MozillaRegisterDebugFILE(f); + + TimeStamp now = TimeStamp::Now(); + MOZ_ASSERT(now >= gRecordedShutdownStartTime); + TimeDuration diff = now - gRecordedShutdownStartTime; + uint32_t diff2 = diff.ToMilliseconds(); + int written = fprintf(f, "%d\n", diff2); + MozillaUnRegisterDebugFILE(f); + int rv = fclose(f); + if (written < 0 || rv != 0) { + PR_Delete(tmpName.get()); + return; + } + PR_Delete(name.get()); + PR_Rename(tmpName.get(), name.get()); +} + +} // namespace mozilla + + +//////////////////////////////////////////////////////////////////////// +//////////////////////////////////////////////////////////////////////// +// +// EXTERNALLY VISIBLE FUNCTIONS in mozilla::Telemetry:: +// These are NOT listed in Telemetry.h + +namespace mozilla { +namespace Telemetry { + +ProcessedStack::ProcessedStack() +{ +} + +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) +{ + 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); + } + +#ifdef MOZ_ENABLE_PROFILER_SPS + // Remove all modules not referenced by a PC on the stack + std::sort(rawStack.begin(), rawStack.end(), CompareByPC); + + size_t moduleIndex = 0; + size_t stackIndex = 0; + size_t stackSize = rawStack.size(); + + SharedLibraryInfo rawModules = SharedLibraryInfo::GetInfoForSelf(); + rawModules.SortByAddress(); + + 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 = rawStack[stackIndex].mPC; + if (pc >= moduleEnd) + break; + + if (pc >= moduleStart) { + // If the current PC is within the current module, mark + // module as used + moduleReferenced = true; + rawStack[stackIndex].mPC -= moduleStart; + rawStack[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. + rawStack[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. + rawStack[stackIndex].mPC = std::numeric_limits<uintptr_t>::max(); + } + + std::sort(rawStack.begin(), rawStack.end(), CompareByIndex); +#endif + + // Copy the information to the return value. + ProcessedStack Ret; + for (std::vector<StackFrame>::iterator i = rawStack.begin(), + e = rawStack.end(); i != e; ++i) { + const StackFrame &rawFrame = *i; + mozilla::Telemetry::ProcessedStack::Frame frame = { rawFrame.mPC, rawFrame.mModIndex }; + Ret.AddFrame(frame); + } + +#ifdef MOZ_ENABLE_PROFILER_SPS + for (unsigned i = 0, n = rawModules.GetSize(); i != n; ++i) { + const SharedLibrary &info = rawModules.GetEntry(i); + const std::string &name = info.GetName(); + std::string basename = name; +#ifdef XP_MACOSX + // FIXME: We want to use just the basename as the libname, but the + // current profiler addon needs the full path name, so we compute the + // basename in here. + size_t pos = name.rfind('/'); + if (pos != std::string::npos) { + basename = name.substr(pos + 1); + } +#endif + mozilla::Telemetry::ProcessedStack::Module module = { + basename, + info.GetBreakpadId() + }; + Ret.AddModule(module); + } +#endif + + return Ret; +} + +void +TimeHistogram::Add(PRIntervalTime aTime) +{ + uint32_t timeMs = PR_IntervalToMilliseconds(aTime); + size_t index = mozilla::FloorLog2(timeMs); + operator[](index)++; +} + +const char* +HangStack::InfallibleAppendViaBuffer(const char* aText, size_t aLength) +{ + MOZ_ASSERT(this->canAppendWithoutRealloc(1)); + // Include null-terminator in length count. + MOZ_ASSERT(mBuffer.canAppendWithoutRealloc(aLength + 1)); + + const char* const entry = mBuffer.end(); + mBuffer.infallibleAppend(aText, aLength); + mBuffer.infallibleAppend('\0'); // Explicitly append null-terminator + this->infallibleAppend(entry); + return entry; +} + +const char* +HangStack::AppendViaBuffer(const char* aText, size_t aLength) +{ + if (!this->reserve(this->length() + 1)) { + return nullptr; + } + + // Keep track of the previous buffer in case we need to adjust pointers later. + const char* const prevStart = mBuffer.begin(); + const char* const prevEnd = mBuffer.end(); + + // Include null-terminator in length count. + if (!mBuffer.reserve(mBuffer.length() + aLength + 1)) { + return nullptr; + } + + if (prevStart != mBuffer.begin()) { + // The buffer has moved; we have to adjust pointers in the stack. + for (const char** entry = this->begin(); entry != this->end(); entry++) { + if (*entry >= prevStart && *entry < prevEnd) { + // Move from old buffer to new buffer. + *entry += mBuffer.begin() - prevStart; + } + } + } + + return InfallibleAppendViaBuffer(aText, aLength); +} + +uint32_t +HangHistogram::GetHash(const HangStack& aStack) +{ + uint32_t hash = 0; + for (const char* const* label = aStack.begin(); + label != aStack.end(); label++) { + /* If the string is within our buffer, we need to hash its content. + Otherwise, the string is statically allocated, and we only need + to hash the pointer instead of the content. */ + if (aStack.IsInBuffer(*label)) { + hash = AddToHash(hash, HashString(*label)); + } else { + hash = AddToHash(hash, *label); + } + } + return hash; +} + +bool +HangHistogram::operator==(const HangHistogram& aOther) const +{ + if (mHash != aOther.mHash) { + return false; + } + if (mStack.length() != aOther.mStack.length()) { + return false; + } + return mStack == aOther.mStack; +} + +} // namespace Telemetry +} // namespace mozilla + + +//////////////////////////////////////////////////////////////////////// +//////////////////////////////////////////////////////////////////////// +// +// EXTERNALLY VISIBLE FUNCTIONS in mozilla::Telemetry:: +// These are listed in Telemetry.h + +namespace mozilla { +namespace Telemetry { + +// The external API for controlling recording state +void +SetHistogramRecordingEnabled(ID aID, bool aEnabled) +{ + TelemetryHistogram::SetHistogramRecordingEnabled(aID, aEnabled); +} + +void +Accumulate(ID aHistogram, uint32_t aSample) +{ + TelemetryHistogram::Accumulate(aHistogram, aSample); +} + +void +Accumulate(ID aID, const nsCString& aKey, uint32_t aSample) +{ + TelemetryHistogram::Accumulate(aID, aKey, aSample); +} + +void +Accumulate(const char* name, uint32_t sample) +{ + TelemetryHistogram::Accumulate(name, sample); +} + +void +Accumulate(const char *name, const nsCString& key, uint32_t sample) +{ + TelemetryHistogram::Accumulate(name, key, sample); +} + +void +AccumulateCategorical(ID id, const nsCString& label) +{ + TelemetryHistogram::AccumulateCategorical(id, label); +} + +void +AccumulateTimeDelta(ID aHistogram, TimeStamp start, TimeStamp end) +{ + Accumulate(aHistogram, + static_cast<uint32_t>((end - start).ToMilliseconds())); +} + +void +AccumulateChild(GeckoProcessType aProcessType, + const nsTArray<Accumulation>& aAccumulations) +{ + TelemetryHistogram::AccumulateChild(aProcessType, aAccumulations); +} + +void +AccumulateChildKeyed(GeckoProcessType aProcessType, + const nsTArray<KeyedAccumulation>& aAccumulations) +{ + TelemetryHistogram::AccumulateChildKeyed(aProcessType, aAccumulations); +} + +const char* +GetHistogramName(ID id) +{ + return TelemetryHistogram::GetHistogramName(id); +} + +bool +CanRecordBase() +{ + return TelemetryHistogram::CanRecordBase(); +} + +bool +CanRecordExtended() +{ + return TelemetryHistogram::CanRecordExtended(); +} + +void +RecordSlowSQLStatement(const nsACString &statement, + const nsACString &dbName, + uint32_t delay) +{ + TelemetryImpl::RecordSlowStatement(statement, dbName, delay); +} + +void +RecordWebrtcIceCandidates(const uint32_t iceCandidateBitmask, + const bool success) +{ + TelemetryImpl::RecordIceCandidates(iceCandidateBitmask, success); +} + +void Init() +{ + // Make the service manager hold a long-lived reference to the service + nsCOMPtr<nsITelemetry> telemetryService = + do_GetService("@mozilla.org/base/telemetry;1"); + MOZ_ASSERT(telemetryService); +} + +#if defined(MOZ_ENABLE_PROFILER_SPS) +void RecordChromeHang(uint32_t duration, + ProcessedStack &aStack, + int32_t aSystemUptime, + int32_t aFirefoxUptime, + HangAnnotationsPtr aAnnotations) +{ + TelemetryImpl::RecordChromeHang(duration, aStack, + aSystemUptime, aFirefoxUptime, + Move(aAnnotations)); +} +#endif + +void RecordThreadHangStats(ThreadHangStats& aStats) +{ + TelemetryImpl::RecordThreadHangStats(aStats); +} + + +void +WriteFailedProfileLock(nsIFile* aProfileDir) +{ + nsCOMPtr<nsIFile> file; + nsresult rv = GetFailedProfileLockFile(getter_AddRefs(file), aProfileDir); + NS_ENSURE_SUCCESS_VOID(rv); + int64_t fileSize = 0; + rv = file->GetFileSize(&fileSize); + // It's expected that the file might not exist yet + if (NS_FAILED(rv) && rv != NS_ERROR_FILE_NOT_FOUND) { + return; + } + nsCOMPtr<nsIFileStream> fileStream; + rv = NS_NewLocalFileStream(getter_AddRefs(fileStream), file, + PR_RDWR | PR_CREATE_FILE, 0640); + NS_ENSURE_SUCCESS_VOID(rv); + NS_ENSURE_TRUE_VOID(fileSize <= kMaxFailedProfileLockFileSize); + unsigned int failedLockCount = 0; + if (fileSize > 0) { + nsCOMPtr<nsIInputStream> inStream = do_QueryInterface(fileStream); + NS_ENSURE_TRUE_VOID(inStream); + if (!GetFailedLockCount(inStream, fileSize, failedLockCount)) { + failedLockCount = 0; + } + } + ++failedLockCount; + nsAutoCString bufStr; + bufStr.AppendInt(static_cast<int>(failedLockCount)); + nsCOMPtr<nsISeekableStream> seekStream = do_QueryInterface(fileStream); + NS_ENSURE_TRUE_VOID(seekStream); + // If we read in an existing failed lock count, we need to reset the file ptr + if (fileSize > 0) { + rv = seekStream->Seek(nsISeekableStream::NS_SEEK_SET, 0); + NS_ENSURE_SUCCESS_VOID(rv); + } + nsCOMPtr<nsIOutputStream> outStream = do_QueryInterface(fileStream); + uint32_t bytesLeft = bufStr.Length(); + const char* bytes = bufStr.get(); + do { + uint32_t written = 0; + rv = outStream->Write(bytes, bytesLeft, &written); + if (NS_FAILED(rv)) { + break; + } + bytes += written; + bytesLeft -= written; + } while (bytesLeft > 0); + seekStream->SetEOF(); +} + +void +InitIOReporting(nsIFile* aXreDir) +{ + // Never initialize twice + if (sTelemetryIOObserver) { + return; + } + + sTelemetryIOObserver = new TelemetryIOInterposeObserver(aXreDir); + IOInterposer::Register(IOInterposeObserver::OpAllWithStaging, + sTelemetryIOObserver); +} + +void +SetProfileDir(nsIFile* aProfD) +{ + if (!sTelemetryIOObserver || !aProfD) { + return; + } + nsAutoString profDirPath; + nsresult rv = aProfD->GetPath(profDirPath); + if (NS_FAILED(rv)) { + return; + } + sTelemetryIOObserver->AddPath(profDirPath, NS_LITERAL_STRING("{profile}")); +} + +void CreateStatisticsRecorder() +{ + TelemetryHistogram::CreateStatisticsRecorder(); +} + +void DestroyStatisticsRecorder() +{ + TelemetryHistogram::DestroyStatisticsRecorder(); +} + +// Scalar API C++ Endpoints + +void +ScalarAdd(mozilla::Telemetry::ScalarID aId, uint32_t aVal) +{ + TelemetryScalar::Add(aId, aVal); +} + +void +ScalarSet(mozilla::Telemetry::ScalarID aId, uint32_t aVal) +{ + TelemetryScalar::Set(aId, aVal); +} + +void +ScalarSet(mozilla::Telemetry::ScalarID aId, bool aVal) +{ + TelemetryScalar::Set(aId, aVal); +} + +void +ScalarSet(mozilla::Telemetry::ScalarID aId, const nsAString& aVal) +{ + TelemetryScalar::Set(aId, aVal); +} + +void +ScalarSetMaximum(mozilla::Telemetry::ScalarID aId, uint32_t aVal) +{ + TelemetryScalar::SetMaximum(aId, aVal); +} + +void +ScalarAdd(mozilla::Telemetry::ScalarID aId, const nsAString& aKey, uint32_t aVal) +{ + TelemetryScalar::Add(aId, aKey, aVal); +} + +void +ScalarSet(mozilla::Telemetry::ScalarID aId, const nsAString& aKey, uint32_t aVal) +{ + TelemetryScalar::Set(aId, aKey, aVal); +} + +void +ScalarSet(mozilla::Telemetry::ScalarID aId, const nsAString& aKey, bool aVal) +{ + TelemetryScalar::Set(aId, aKey, aVal); +} + +void +ScalarSetMaximum(mozilla::Telemetry::ScalarID aId, const nsAString& aKey, uint32_t aVal) +{ + TelemetryScalar::SetMaximum(aId, aKey, aVal); +} + +} // namespace Telemetry +} // namespace mozilla |