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/core/TelemetryHistogram.cpp | |
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/core/TelemetryHistogram.cpp')
-rw-r--r-- | toolkit/components/telemetry/core/TelemetryHistogram.cpp | 3658 |
1 files changed, 3658 insertions, 0 deletions
diff --git a/toolkit/components/telemetry/core/TelemetryHistogram.cpp b/toolkit/components/telemetry/core/TelemetryHistogram.cpp new file mode 100644 index 0000000000..047682285c --- /dev/null +++ b/toolkit/components/telemetry/core/TelemetryHistogram.cpp @@ -0,0 +1,3658 @@ +/* -*- 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 "TelemetryHistogram.h" + +#include <limits> +#include "base/histogram.h" +#include "geckoview/streaming/GeckoViewStreamingTelemetry.h" +#include "ipc/TelemetryIPCAccumulator.h" +#include "jsapi.h" +#include "jsfriendapi.h" +#include "js/Array.h" // JS::GetArrayLength, JS::IsArrayObject, JS::NewArrayObject +#include "js/GCAPI.h" +#include "js/Object.h" // JS::GetClass, JS::GetPrivate, JS::SetPrivate +#include "mozilla/dom/ToJSValue.h" +#include "mozilla/gfx/GPUProcessManager.h" +#include "mozilla/Atomics.h" +#include "mozilla/JSONWriter.h" +#include "mozilla/StartupTimeline.h" +#include "mozilla/StaticMutex.h" +#include "mozilla/Unused.h" +#include "nsClassHashtable.h" +#include "nsString.h" +#include "nsHashKeys.h" +#include "nsITelemetry.h" +#include "nsPrintfCString.h" +#include "TelemetryHistogramNameMap.h" +#include "TelemetryScalar.h" + +using base::BooleanHistogram; +using base::CountHistogram; +using base::FlagHistogram; +using base::LinearHistogram; +using mozilla::MakeTuple; +using mozilla::StaticMutex; +using mozilla::StaticMutexAutoLock; +using mozilla::Telemetry::HistogramAccumulation; +using mozilla::Telemetry::HistogramCount; +using mozilla::Telemetry::HistogramID; +using mozilla::Telemetry::HistogramIDByNameLookup; +using mozilla::Telemetry::KeyedHistogramAccumulation; +using mozilla::Telemetry::ProcessID; +using mozilla::Telemetry::Common::CanRecordDataset; +using mozilla::Telemetry::Common::CanRecordProduct; +using mozilla::Telemetry::Common::GetCurrentProduct; +using mozilla::Telemetry::Common::GetIDForProcessName; +using mozilla::Telemetry::Common::GetNameForProcessID; +using mozilla::Telemetry::Common::IsExpiredVersion; +using mozilla::Telemetry::Common::IsInDataset; +using mozilla::Telemetry::Common::LogToBrowserConsole; +using mozilla::Telemetry::Common::RecordedProcessType; +using mozilla::Telemetry::Common::StringHashSet; +using mozilla::Telemetry::Common::SupportedProduct; +using mozilla::Telemetry::Common::ToJSString; + +namespace TelemetryIPCAccumulator = mozilla::TelemetryIPCAccumulator; + +//////////////////////////////////////////////////////////////////////// +//////////////////////////////////////////////////////////////////////// +// +// Naming: there are two kinds of functions in this file: +// +// * Functions named internal_*: these can only be reached via an +// interface function (TelemetryHistogram::*). They mostly expect +// the interface function to have acquired +// |gTelemetryHistogramMutex|, so they do not have to be +// thread-safe. However, those internal_* functions that are +// reachable from internal_WrapAndReturnHistogram and +// internal_WrapAndReturnKeyedHistogram can sometimes be called +// without |gTelemetryHistogramMutex|, and so might be racey. +// +// * Functions named TelemetryHistogram::*. This is the external interface. +// Entries and exits to these functions are serialised using +// |gTelemetryHistogramMutex|, except for GetKeyedHistogramSnapshots and +// CreateHistogramSnapshots. +// +// Avoiding races and deadlocks: +// +// All functions in the external interface (TelemetryHistogram::*) are +// serialised using the mutex |gTelemetryHistogramMutex|. This means +// that the external interface is thread-safe, and many of the +// internal_* functions can ignore thread safety. But it also brings +// a danger of deadlock if any function in the external interface can +// get back to that interface. That is, we will deadlock on any call +// chain like this +// +// TelemetryHistogram::* -> .. any functions .. -> TelemetryHistogram::* +// +// To reduce the danger of that happening, observe the following rules: +// +// * No function in TelemetryHistogram::* may directly call, nor take the +// address of, any other function in TelemetryHistogram::*. +// +// * No internal function internal_* may call, nor take the address +// of, any function in TelemetryHistogram::*. +// +// internal_WrapAndReturnHistogram and +// internal_WrapAndReturnKeyedHistogram are not protected by +// |gTelemetryHistogramMutex| because they make calls to the JS +// engine, but that can in turn call back to Telemetry and hence back +// to a TelemetryHistogram:: function, in order to report GC and other +// statistics. This would lead to deadlock due to attempted double +// acquisition of |gTelemetryHistogramMutex|, if the internal_* functions +// were required to be protected by |gTelemetryHistogramMutex|. To +// break that cycle, we relax that requirement. Unfortunately this +// means that this file is not guaranteed race-free. + +// This is a StaticMutex rather than a plain Mutex (1) so that +// it gets initialised in a thread-safe manner the first time +// it is used, and (2) because it is never de-initialised, and +// a normal Mutex would show up as a leak in BloatView. StaticMutex +// also has the "OffTheBooks" property, so it won't show as a leak +// in BloatView. +static StaticMutex gTelemetryHistogramMutex; + +//////////////////////////////////////////////////////////////////////// +//////////////////////////////////////////////////////////////////////// +// +// PRIVATE TYPES + +namespace { + +// Hardcoded probes +// +// The order of elements here is important to minimize the memory footprint of a +// HistogramInfo instance. +// +// Any adjustements need to be reflected in gen_histogram_data.py +struct HistogramInfo { + uint32_t min; + uint32_t max; + uint32_t bucketCount; + uint32_t name_offset; + uint32_t expiration_offset; + uint32_t label_count; + uint32_t key_count; + uint32_t store_count; + uint16_t label_index; + uint16_t key_index; + uint16_t store_index; + RecordedProcessType record_in_processes; + bool keyed; + uint8_t histogramType; + uint8_t dataset; + SupportedProduct products; + + const char* name() const; + const char* expiration() const; + nsresult label_id(const char* label, uint32_t* labelId) const; + bool allows_key(const nsACString& key) const; + bool is_single_store() const; +}; + +// Structs used to keep information about the histograms for which a +// snapshot should be created. +struct HistogramSnapshotData { + CopyableTArray<base::Histogram::Sample> mBucketRanges; + CopyableTArray<base::Histogram::Count> mBucketCounts; + int64_t mSampleSum; // Same type as base::Histogram::SampleSet::sum_ +}; + +struct HistogramSnapshotInfo { + HistogramSnapshotData data; + HistogramID histogramID; +}; + +typedef mozilla::Vector<HistogramSnapshotInfo> HistogramSnapshotsArray; +typedef mozilla::Vector<HistogramSnapshotsArray> HistogramProcessSnapshotsArray; + +// The following is used to handle snapshot information for keyed histograms. +typedef nsDataHashtable<nsCStringHashKey, HistogramSnapshotData> + KeyedHistogramSnapshotData; + +struct KeyedHistogramSnapshotInfo { + KeyedHistogramSnapshotData data; + HistogramID histogramId; +}; + +typedef mozilla::Vector<KeyedHistogramSnapshotInfo> + KeyedHistogramSnapshotsArray; +typedef mozilla::Vector<KeyedHistogramSnapshotsArray> + KeyedHistogramProcessSnapshotsArray; + +/** + * A Histogram storage engine. + * + * Takes care of recording data into multiple stores if necessary. + */ +class Histogram { + public: + /* + * Create a new histogram instance from the given info. + * + * If the histogram is already expired, this does not allocate. + */ + Histogram(HistogramID histogramId, const HistogramInfo& info, bool expired); + ~Histogram(); + + /** + * Add a sample to this histogram in all registered stores. + */ + void Add(uint32_t sample); + + /** + * Clear the named store for this histogram. + */ + void Clear(const nsACString& store); + + /** + * Get the histogram instance from the named store. + */ + bool GetHistogram(const nsACString& store, base::Histogram** h); + + bool IsExpired() const { return mIsExpired; } + + size_t SizeOfIncludingThis(mozilla::MallocSizeOf aMallocSizeOf); + + private: + // String -> Histogram* + typedef nsClassHashtable<nsCStringHashKey, base::Histogram> HistogramStore; + HistogramStore mStorage; + + // A valid pointer if this histogram belongs to only the main store + base::Histogram* mSingleStore; + + // We don't track stores for expired histograms. + // We just store a single flag and all other operations become a no-op. + bool mIsExpired; +}; + +class KeyedHistogram { + public: + KeyedHistogram(HistogramID id, const HistogramInfo& info, bool expired); + ~KeyedHistogram(); + nsresult GetHistogram(const nsCString& aStore, const nsCString& key, + base::Histogram** histogram); + base::Histogram* GetHistogram(const nsCString& aStore, const nsCString& key); + uint32_t GetHistogramType() const { return mHistogramInfo.histogramType; } + nsresult GetKeys(const StaticMutexAutoLock& aLock, const nsCString& store, + nsTArray<nsCString>& aKeys); + // Note: unlike other methods, GetJSSnapshot is thread safe. + nsresult GetJSSnapshot(JSContext* cx, JS::Handle<JSObject*> obj, + const nsACString& aStore, bool clearSubsession); + nsresult GetSnapshot(const StaticMutexAutoLock& aLock, + const nsACString& aStore, + KeyedHistogramSnapshotData& aSnapshot, + bool aClearSubsession); + + nsresult Add(const nsCString& key, uint32_t aSample, ProcessID aProcessType); + void Clear(const nsACString& aStore); + + HistogramID GetHistogramID() const { return mId; } + + bool IsEmpty(const nsACString& aStore) const; + + bool IsExpired() const { return mIsExpired; } + + size_t SizeOfIncludingThis(mozilla::MallocSizeOf aMallocSizeOf); + + private: + typedef nsClassHashtable<nsCStringHashKey, base::Histogram> + KeyedHistogramMapType; + typedef nsClassHashtable<nsCStringHashKey, KeyedHistogramMapType> + StoreMapType; + + StoreMapType mStorage; + // A valid map if this histogram belongs to only the main store + KeyedHistogramMapType* mSingleStore; + + const HistogramID mId; + const HistogramInfo& mHistogramInfo; + bool mIsExpired; +}; + +} // namespace + +//////////////////////////////////////////////////////////////////////// +//////////////////////////////////////////////////////////////////////// +// +// PRIVATE STATE, SHARED BY ALL THREADS + +namespace { + +// Set to true once this global state has been initialized +bool gInitDone = false; + +// Whether we are collecting the base, opt-out, Histogram data. +bool gCanRecordBase = false; +// Whether we are collecting the extended, opt-in, Histogram data. +bool gCanRecordExtended = false; + +// The storage for actual Histogram instances. +// We use separate ones for plain and keyed histograms. +Histogram** gHistogramStorage; +// Keyed histograms internally map string keys to individual Histogram +// instances. +KeyedHistogram** gKeyedHistogramStorage; + +// To simplify logic below we use a single histogram instance for all expired +// histograms. +Histogram* gExpiredHistogram = nullptr; + +// The single placeholder for expired keyed histograms. +KeyedHistogram* gExpiredKeyedHistogram = nullptr; + +// This tracks whether recording is enabled for specific histograms. +// To utilize C++ initialization rules, we invert the meaning to "disabled". +bool gHistogramRecordingDisabled[HistogramCount] = {}; + +// This is for gHistogramInfos, gHistogramStringTable +#include "TelemetryHistogramData.inc" + +} // namespace + +//////////////////////////////////////////////////////////////////////// +//////////////////////////////////////////////////////////////////////// +// +// PRIVATE CONSTANTS + +namespace { + +// List of histogram IDs which should have recording disabled initially. +const HistogramID kRecordingInitiallyDisabledIDs[] = { + mozilla::Telemetry::FX_REFRESH_DRIVER_SYNC_SCROLL_FRAME_DELAY_MS, + + // The array must not be empty. Leave these item here. + mozilla::Telemetry::TELEMETRY_TEST_COUNT_INIT_NO_RECORD, + mozilla::Telemetry::TELEMETRY_TEST_KEYED_COUNT_INIT_NO_RECORD}; + +const char* TEST_HISTOGRAM_PREFIX = "TELEMETRY_TEST_"; + +} // namespace + +//////////////////////////////////////////////////////////////////////// +//////////////////////////////////////////////////////////////////////// +// +// The core storage access functions. +// They wrap access to the histogram storage and lookup caches. + +namespace { + +size_t internal_KeyedHistogramStorageIndex(HistogramID aHistogramId, + ProcessID aProcessId) { + return aHistogramId * size_t(ProcessID::Count) + size_t(aProcessId); +} + +size_t internal_HistogramStorageIndex(const StaticMutexAutoLock& aLock, + HistogramID aHistogramId, + ProcessID aProcessId) { + static_assert(HistogramCount < std::numeric_limits<size_t>::max() / + size_t(ProcessID::Count), + "Too many histograms and processes to store in a 1D array."); + + return aHistogramId * size_t(ProcessID::Count) + size_t(aProcessId); +} + +Histogram* internal_GetHistogramFromStorage(const StaticMutexAutoLock& aLock, + HistogramID aHistogramId, + ProcessID aProcessId) { + size_t index = + internal_HistogramStorageIndex(aLock, aHistogramId, aProcessId); + return gHistogramStorage[index]; +} + +void internal_SetHistogramInStorage(const StaticMutexAutoLock& aLock, + HistogramID aHistogramId, + ProcessID aProcessId, + Histogram* aHistogram) { + MOZ_ASSERT(XRE_IsParentProcess(), + "Histograms are stored only in the parent process."); + + size_t index = + internal_HistogramStorageIndex(aLock, aHistogramId, aProcessId); + MOZ_ASSERT(!gHistogramStorage[index], + "Mustn't overwrite storage without clearing it first."); + gHistogramStorage[index] = aHistogram; +} + +KeyedHistogram* internal_GetKeyedHistogramFromStorage(HistogramID aHistogramId, + ProcessID aProcessId) { + size_t index = internal_KeyedHistogramStorageIndex(aHistogramId, aProcessId); + return gKeyedHistogramStorage[index]; +} + +void internal_SetKeyedHistogramInStorage(HistogramID aHistogramId, + ProcessID aProcessId, + KeyedHistogram* aKeyedHistogram) { + MOZ_ASSERT(XRE_IsParentProcess(), + "Keyed Histograms are stored only in the parent process."); + + size_t index = internal_KeyedHistogramStorageIndex(aHistogramId, aProcessId); + MOZ_ASSERT(!gKeyedHistogramStorage[index], + "Mustn't overwrite storage without clearing it first"); + gKeyedHistogramStorage[index] = aKeyedHistogram; +} + +// Factory function for base::Histogram instances. +base::Histogram* internal_CreateBaseHistogramInstance(const HistogramInfo& info, + int bucketsOffset); + +// Factory function for histogram instances. +Histogram* internal_CreateHistogramInstance(HistogramID histogramId); + +bool internal_IsHistogramEnumId(HistogramID aID) { + static_assert(((HistogramID)-1 > 0), "ID should be unsigned."); + return aID < HistogramCount; +} + +// Look up a plain histogram by id. +Histogram* internal_GetHistogramById(const StaticMutexAutoLock& aLock, + HistogramID histogramId, + ProcessID processId, + bool instantiate = true) { + MOZ_ASSERT(internal_IsHistogramEnumId(histogramId)); + MOZ_ASSERT(!gHistogramInfos[histogramId].keyed); + MOZ_ASSERT(processId < ProcessID::Count); + + Histogram* h = + internal_GetHistogramFromStorage(aLock, histogramId, processId); + if (h || !instantiate) { + return h; + } + + h = internal_CreateHistogramInstance(histogramId); + MOZ_ASSERT(h); + internal_SetHistogramInStorage(aLock, histogramId, processId, h); + + return h; +} + +// Look up a keyed histogram by id. +KeyedHistogram* internal_GetKeyedHistogramById(HistogramID histogramId, + ProcessID processId, + bool instantiate = true) { + MOZ_ASSERT(internal_IsHistogramEnumId(histogramId)); + MOZ_ASSERT(gHistogramInfos[histogramId].keyed); + MOZ_ASSERT(processId < ProcessID::Count); + + KeyedHistogram* kh = + internal_GetKeyedHistogramFromStorage(histogramId, processId); + if (kh || !instantiate) { + return kh; + } + + const HistogramInfo& info = gHistogramInfos[histogramId]; + const bool isExpired = IsExpiredVersion(info.expiration()); + + // If the keyed histogram is expired, set its storage to the expired + // keyed histogram. + if (isExpired) { + if (!gExpiredKeyedHistogram) { + // If we don't have an expired keyed histogram, create one. + gExpiredKeyedHistogram = + new KeyedHistogram(histogramId, info, true /* expired */); + MOZ_ASSERT(gExpiredKeyedHistogram); + } + kh = gExpiredKeyedHistogram; + } else { + kh = new KeyedHistogram(histogramId, info, false /* expired */); + } + + internal_SetKeyedHistogramInStorage(histogramId, processId, kh); + + return kh; +} + +// Look up a histogram id from a histogram name. +nsresult internal_GetHistogramIdByName(const StaticMutexAutoLock& aLock, + const nsACString& name, + HistogramID* id) { + const uint32_t idx = HistogramIDByNameLookup(name); + MOZ_ASSERT(idx < HistogramCount, + "Intermediate lookup should always give a valid index."); + + // The lookup hashes the input and uses it as an index into the value array. + // Hash collisions can still happen for unknown values, + // therefore we check that the name matches. + if (name.Equals(gHistogramInfos[idx].name())) { + *id = HistogramID(idx); + return NS_OK; + } + + return NS_ERROR_ILLEGAL_VALUE; +} + +} // namespace + +//////////////////////////////////////////////////////////////////////// +//////////////////////////////////////////////////////////////////////// +// +// PRIVATE: Misc small helpers + +namespace { + +bool internal_CanRecordBase() { return gCanRecordBase; } + +bool internal_CanRecordExtended() { return gCanRecordExtended; } + +bool internal_AttemptedGPUProcess() { + // Check if it was tried to launch a process. + bool attemptedGPUProcess = false; + if (auto gpm = mozilla::gfx::GPUProcessManager::Get()) { + attemptedGPUProcess = gpm->AttemptedGPUProcess(); + } + return attemptedGPUProcess; +} + +// Note: this is completely unrelated to mozilla::IsEmpty. +bool internal_IsEmpty(const StaticMutexAutoLock& aLock, + const base::Histogram* h) { + return h->is_empty(); +} + +void internal_SetHistogramRecordingEnabled(const StaticMutexAutoLock& aLock, + HistogramID id, bool aEnabled) { + MOZ_ASSERT(internal_IsHistogramEnumId(id)); + gHistogramRecordingDisabled[id] = !aEnabled; +} + +bool internal_IsRecordingEnabled(HistogramID id) { + MOZ_ASSERT(internal_IsHistogramEnumId(id)); + return !gHistogramRecordingDisabled[id]; +} + +const char* HistogramInfo::name() const { + return &gHistogramStringTable[this->name_offset]; +} + +const char* HistogramInfo::expiration() const { + return &gHistogramStringTable[this->expiration_offset]; +} + +nsresult HistogramInfo::label_id(const char* label, uint32_t* labelId) const { + MOZ_ASSERT(label); + MOZ_ASSERT(this->histogramType == nsITelemetry::HISTOGRAM_CATEGORICAL); + if (this->histogramType != nsITelemetry::HISTOGRAM_CATEGORICAL) { + return NS_ERROR_FAILURE; + } + + for (uint32_t i = 0; i < this->label_count; ++i) { + // gHistogramLabelTable contains the indices of the label strings in the + // gHistogramStringTable. + // They are stored in-order and consecutively, from the offset label_index + // to (label_index + label_count). + uint32_t string_offset = gHistogramLabelTable[this->label_index + i]; + const char* const str = &gHistogramStringTable[string_offset]; + if (::strcmp(label, str) == 0) { + *labelId = i; + return NS_OK; + } + } + + return NS_ERROR_FAILURE; +} + +bool HistogramInfo::allows_key(const nsACString& key) const { + MOZ_ASSERT(this->keyed); + + // If we didn't specify a list of allowed keys, just return true. + if (this->key_count == 0) { + return true; + } + + // Otherwise, check if |key| is in the list of allowed keys. + for (uint32_t i = 0; i < this->key_count; ++i) { + // gHistogramKeyTable contains the indices of the key strings in the + // gHistogramStringTable. They are stored in-order and consecutively, + // from the offset key_index to (key_index + key_count). + uint32_t string_offset = gHistogramKeyTable[this->key_index + i]; + const char* const str = &gHistogramStringTable[string_offset]; + if (key.EqualsASCII(str)) { + return true; + } + } + + // |key| was not found. + return false; +} + +bool HistogramInfo::is_single_store() const { + return store_count == 1 && store_index == UINT16_MAX; +} + +} // namespace + +//////////////////////////////////////////////////////////////////////// +//////////////////////////////////////////////////////////////////////// +// +// PRIVATE: Histogram Get, Add, Clone, Clear functions + +namespace { + +nsresult internal_CheckHistogramArguments(const HistogramInfo& info) { + if (info.histogramType != nsITelemetry::HISTOGRAM_BOOLEAN && + info.histogramType != nsITelemetry::HISTOGRAM_FLAG && + info.histogramType != nsITelemetry::HISTOGRAM_COUNT) { + // Sanity checks for histogram parameters. + if (info.min >= info.max) { + return NS_ERROR_ILLEGAL_VALUE; + } + + if (info.bucketCount <= 2) { + return NS_ERROR_ILLEGAL_VALUE; + } + + if (info.min < 1) { + return NS_ERROR_ILLEGAL_VALUE; + } + } + + return NS_OK; +} + +Histogram* internal_CreateHistogramInstance(HistogramID histogramId) { + const HistogramInfo& info = gHistogramInfos[histogramId]; + + if (NS_FAILED(internal_CheckHistogramArguments(info))) { + MOZ_ASSERT(false, "Failed histogram argument checks."); + return nullptr; + } + + const bool isExpired = IsExpiredVersion(info.expiration()); + + if (isExpired) { + if (!gExpiredHistogram) { + gExpiredHistogram = new Histogram(histogramId, info, /* expired */ true); + } + + return gExpiredHistogram; + } + + Histogram* wrapper = new Histogram(histogramId, info, /* expired */ false); + + return wrapper; +} + +base::Histogram* internal_CreateBaseHistogramInstance( + const HistogramInfo& passedInfo, int bucketsOffset) { + if (NS_FAILED(internal_CheckHistogramArguments(passedInfo))) { + MOZ_ASSERT(false, "Failed histogram argument checks."); + return nullptr; + } + + // We don't actually store data for expired histograms at all. + MOZ_ASSERT(!IsExpiredVersion(passedInfo.expiration())); + + HistogramInfo info = passedInfo; + const int* buckets = &gHistogramBucketLowerBounds[bucketsOffset]; + + base::Histogram::Flags flags = base::Histogram::kNoFlags; + base::Histogram* h = nullptr; + switch (info.histogramType) { + case nsITelemetry::HISTOGRAM_EXPONENTIAL: + h = base::Histogram::FactoryGet(info.min, info.max, info.bucketCount, + flags, buckets); + break; + case nsITelemetry::HISTOGRAM_LINEAR: + case nsITelemetry::HISTOGRAM_CATEGORICAL: + h = LinearHistogram::FactoryGet(info.min, info.max, info.bucketCount, + flags, buckets); + break; + case nsITelemetry::HISTOGRAM_BOOLEAN: + h = BooleanHistogram::FactoryGet(flags, buckets); + break; + case nsITelemetry::HISTOGRAM_FLAG: + h = FlagHistogram::FactoryGet(flags, buckets); + break; + case nsITelemetry::HISTOGRAM_COUNT: + h = CountHistogram::FactoryGet(flags, buckets); + break; + default: + MOZ_ASSERT(false, "Invalid histogram type"); + return nullptr; + } + + return h; +} + +nsresult internal_HistogramAdd(const StaticMutexAutoLock& aLock, + Histogram& histogram, const HistogramID id, + uint32_t value, ProcessID aProcessType) { + // Check if we are allowed to record the data. + bool canRecordDataset = + CanRecordDataset(gHistogramInfos[id].dataset, internal_CanRecordBase(), + internal_CanRecordExtended()); + // If `histogram` is a non-parent-process histogram, then recording-enabled + // has been checked in its owner process. + if (!canRecordDataset || + (aProcessType == ProcessID::Parent && !internal_IsRecordingEnabled(id))) { + return NS_OK; + } + + // Don't record if the current platform is not enabled + if (!CanRecordProduct(gHistogramInfos[id].products)) { + return NS_OK; + } + + if (&histogram != gExpiredHistogram && + GetCurrentProduct() == SupportedProduct::GeckoviewStreaming) { + const HistogramInfo& info = gHistogramInfos[id]; + GeckoViewStreamingTelemetry::HistogramAccumulate( + nsDependentCString(info.name()), + info.histogramType == nsITelemetry::HISTOGRAM_CATEGORICAL, value); + return NS_OK; + } + + // The internal representation of a base::Histogram's buckets uses `int`. + // Clamp large values of `value` to be INT_MAX so they continue to be treated + // as large values (instead of negative ones). + if (value > INT_MAX) { + TelemetryScalar::Add( + mozilla::Telemetry::ScalarID::TELEMETRY_ACCUMULATE_CLAMPED_VALUES, + NS_ConvertASCIItoUTF16(gHistogramInfos[id].name()), 1); + value = INT_MAX; + } + + histogram.Add(value); + + return NS_OK; +} + +} // namespace + +//////////////////////////////////////////////////////////////////////// +//////////////////////////////////////////////////////////////////////// +// +// PRIVATE: Histogram reflection helpers + +namespace { + +/** + * Copy histograms and samples to Mozilla-friendly structures. + * Please note that this version does not make use of JS contexts. + * + * @param {StaticMutexAutoLock} the proof we hold the mutex. + * @param {Histogram} the histogram to reflect. + * @return {nsresult} NS_ERROR_FAILURE if we fail to allocate memory for the + * snapshot. + */ +nsresult internal_GetHistogramAndSamples(const StaticMutexAutoLock& aLock, + const base::Histogram* h, + HistogramSnapshotData& aSnapshot) { + MOZ_ASSERT(h); + + // Convert the ranges of the buckets to a nsTArray. + const size_t bucketCount = h->bucket_count(); + for (size_t i = 0; i < bucketCount; i++) { + // XXX(Bug 1631371) Check if this should use a fallible operation as it + // pretended earlier, or change the return type to void. + aSnapshot.mBucketRanges.AppendElement(h->ranges(i)); + } + + // Get a snapshot of the samples. + base::Histogram::SampleSet ss = h->SnapshotSample(); + + // Get the number of samples in each bucket. + for (size_t i = 0; i < bucketCount; i++) { + // XXX(Bug 1631371) Check if this should use a fallible operation as it + // pretended earlier, or change the return type to void. + aSnapshot.mBucketCounts.AppendElement(ss.counts(i)); + } + + // Finally, save the |sum|. We don't need to reflect declared_min, + // declared_max and histogram_type as they are in gHistogramInfo. + aSnapshot.mSampleSum = ss.sum(); + return NS_OK; +} + +/** + * Reflect a histogram snapshot into a JavaScript object. + * The returned histogram object will have the following properties: + * + * bucket_count - Number of buckets of this histogram + * histogram_type - HISTOGRAM_EXPONENTIAL, HISTOGRAM_LINEAR, + * HISTOGRAM_BOOLEAN, HISTOGRAM_FLAG, HISTOGRAM_COUNT, or HISTOGRAM_CATEGORICAL + * sum - sum of the bucket contents + * range - A 2-item array of minimum and maximum bucket size + * values - Map from bucket start to the bucket's count + */ +nsresult internal_ReflectHistogramAndSamples( + JSContext* cx, JS::Handle<JSObject*> obj, + const HistogramInfo& aHistogramInfo, + const HistogramSnapshotData& aSnapshot) { + if (!(JS_DefineProperty(cx, obj, "bucket_count", aHistogramInfo.bucketCount, + JSPROP_ENUMERATE) && + JS_DefineProperty(cx, obj, "histogram_type", + aHistogramInfo.histogramType, JSPROP_ENUMERATE) && + JS_DefineProperty(cx, obj, "sum", double(aSnapshot.mSampleSum), + JSPROP_ENUMERATE))) { + return NS_ERROR_FAILURE; + } + + // Don't rely on the bucket counts from "aHistogramInfo": it may + // differ from the length of aSnapshot.mBucketCounts due to expired + // histograms. + const size_t count = aSnapshot.mBucketCounts.Length(); + MOZ_ASSERT(count == aSnapshot.mBucketRanges.Length(), + "The number of buckets and the number of counts must match."); + + // Create the "range" property and add it to the final object. + JS::Rooted<JSObject*> rarray(cx, JS::NewArrayObject(cx, 2)); + if (rarray == nullptr || + !JS_DefineProperty(cx, obj, "range", rarray, JSPROP_ENUMERATE)) { + return NS_ERROR_FAILURE; + } + // Add [min, max] into the range array + if (!JS_DefineElement(cx, rarray, 0, aHistogramInfo.min, JSPROP_ENUMERATE)) { + return NS_ERROR_FAILURE; + } + if (!JS_DefineElement(cx, rarray, 1, aHistogramInfo.max, JSPROP_ENUMERATE)) { + return NS_ERROR_FAILURE; + } + + JS::Rooted<JSObject*> values(cx, JS_NewPlainObject(cx)); + if (values == nullptr || + !JS_DefineProperty(cx, obj, "values", values, JSPROP_ENUMERATE)) { + return NS_ERROR_FAILURE; + } + + bool first = true; + size_t last = 0; + + for (size_t i = 0; i < count; i++) { + auto value = aSnapshot.mBucketCounts[i]; + if (value == 0) { + continue; + } + + if (i > 0 && first) { + auto range = aSnapshot.mBucketRanges[i - 1]; + if (!JS_DefineProperty(cx, values, nsPrintfCString("%d", range).get(), 0, + JSPROP_ENUMERATE)) { + return NS_ERROR_FAILURE; + } + } + + first = false; + last = i + 1; + + auto range = aSnapshot.mBucketRanges[i]; + if (!JS_DefineProperty(cx, values, nsPrintfCString("%d", range).get(), + value, JSPROP_ENUMERATE)) { + return NS_ERROR_FAILURE; + } + } + + if (last > 0 && last < count) { + auto range = aSnapshot.mBucketRanges[last]; + if (!JS_DefineProperty(cx, values, nsPrintfCString("%d", range).get(), 0, + JSPROP_ENUMERATE)) { + return NS_ERROR_FAILURE; + } + } + + return NS_OK; +} + +bool internal_ShouldReflectHistogram(const StaticMutexAutoLock& aLock, + base::Histogram* h, HistogramID id) { + // Only flag histograms are serialized when they are empty. + // This has historical reasons, changing this will require downstream changes. + // The cheaper path here is to just deprecate flag histograms in favor + // of scalars. + uint32_t type = gHistogramInfos[id].histogramType; + if (internal_IsEmpty(aLock, h) && type != nsITelemetry::HISTOGRAM_FLAG) { + return false; + } + + // Don't reflect the histogram if it's not allowed in this product. + if (!CanRecordProduct(gHistogramInfos[id].products)) { + return false; + } + + return true; +} + +/** + * Helper function to get a snapshot of the histograms. + * + * @param {aLock} the lock proof. + * @param {aStore} the name of the store to snapshot. + * @param {aDataset} the dataset for which the snapshot is being requested. + * @param {aClearSubsession} whether or not to clear the data after + * taking the snapshot. + * @param {aIncludeGPU} whether or not to include data for the GPU. + * @param {aOutSnapshot} the container in which the snapshot data will be + * stored. + * @return {nsresult} NS_OK if the snapshot was successfully taken or + * NS_ERROR_OUT_OF_MEMORY if it failed to allocate memory. + */ +nsresult internal_GetHistogramsSnapshot( + const StaticMutexAutoLock& aLock, const nsACString& aStore, + unsigned int aDataset, bool aClearSubsession, bool aIncludeGPU, + bool aFilterTest, HistogramProcessSnapshotsArray& aOutSnapshot) { + if (!aOutSnapshot.resize(static_cast<uint32_t>(ProcessID::Count))) { + return NS_ERROR_OUT_OF_MEMORY; + } + + for (uint32_t process = 0; process < static_cast<uint32_t>(ProcessID::Count); + ++process) { + HistogramSnapshotsArray& hArray = aOutSnapshot[process]; + + for (size_t i = 0; i < HistogramCount; ++i) { + const HistogramInfo& info = gHistogramInfos[i]; + if (info.keyed) { + continue; + } + + HistogramID id = HistogramID(i); + + if (!CanRecordInProcess(info.record_in_processes, ProcessID(process)) || + ((ProcessID(process) == ProcessID::Gpu) && !aIncludeGPU)) { + continue; + } + + if (!IsInDataset(info.dataset, aDataset)) { + continue; + } + + bool shouldInstantiate = + info.histogramType == nsITelemetry::HISTOGRAM_FLAG; + Histogram* w = internal_GetHistogramById(aLock, id, ProcessID(process), + shouldInstantiate); + if (!w || w->IsExpired()) { + continue; + } + + base::Histogram* h = nullptr; + if (!w->GetHistogram(aStore, &h)) { + continue; + } + + if (!internal_ShouldReflectHistogram(aLock, h, id)) { + continue; + } + + const char* name = info.name(); + if (aFilterTest && strncmp(TEST_HISTOGRAM_PREFIX, name, + strlen(TEST_HISTOGRAM_PREFIX)) == 0) { + if (aClearSubsession) { + h->Clear(); + } + continue; + } + + HistogramSnapshotData snapshotData; + if (NS_FAILED(internal_GetHistogramAndSamples(aLock, h, snapshotData))) { + continue; + } + + if (!hArray.emplaceBack(HistogramSnapshotInfo{snapshotData, id})) { + return NS_ERROR_OUT_OF_MEMORY; + } + + if (aClearSubsession) { + h->Clear(); + } + } + } + return NS_OK; +} + +} // namespace + +//////////////////////////////////////////////////////////////////////// +//////////////////////////////////////////////////////////////////////// +// +// PRIVATE: class Histogram + +namespace { + +Histogram::Histogram(HistogramID histogramId, const HistogramInfo& info, + bool expired) + : mStorage(), mSingleStore(nullptr), mIsExpired(expired) { + if (IsExpired()) { + return; + } + + base::Histogram* h; + const int bucketsOffset = gHistogramBucketLowerBoundIndex[histogramId]; + + if (info.is_single_store()) { + mSingleStore = internal_CreateBaseHistogramInstance(info, bucketsOffset); + } else { + for (uint32_t i = 0; i < info.store_count; i++) { + auto store = nsDependentCString( + &gHistogramStringTable[gHistogramStoresTable[info.store_index + i]]); + h = internal_CreateBaseHistogramInstance(info, bucketsOffset); + mStorage.Put(store, h); + } + } +} + +Histogram::~Histogram() { delete mSingleStore; } + +void Histogram::Add(uint32_t sample) { + MOZ_ASSERT(XRE_IsParentProcess(), + "Only add to histograms in the parent process"); + if (!XRE_IsParentProcess()) { + return; + } + + if (IsExpired()) { + return; + } + + if (mSingleStore != nullptr) { + mSingleStore->Add(sample); + } else { + for (auto iter = mStorage.Iter(); !iter.Done(); iter.Next()) { + auto& h = iter.Data(); + h->Add(sample); + } + } +} + +void Histogram::Clear(const nsACString& store) { + MOZ_ASSERT(XRE_IsParentProcess(), + "Only clear histograms in the parent process"); + if (!XRE_IsParentProcess()) { + return; + } + + if (mSingleStore != nullptr) { + if (store.EqualsASCII("main")) { + mSingleStore->Clear(); + } + } else { + base::Histogram* h = nullptr; + bool found = GetHistogram(store, &h); + if (!found) { + return; + } + MOZ_ASSERT(h, "Should have found a valid histogram in the named store"); + + h->Clear(); + } +} + +bool Histogram::GetHistogram(const nsACString& store, base::Histogram** h) { + MOZ_ASSERT(!IsExpired()); + if (IsExpired()) { + return false; + } + + if (mSingleStore != nullptr) { + if (store.EqualsASCII("main")) { + *h = mSingleStore; + return true; + } + + return false; + } + + return mStorage.Get(store, h); +} + +size_t Histogram::SizeOfIncludingThis(mozilla::MallocSizeOf aMallocSizeOf) { + size_t n = 0; + n += aMallocSizeOf(this); + /* + * In theory mStorage.SizeOfExcludingThis should included the data part of the + * map, but the numbers seemed low, so we are only taking the shallow size and + * do the iteration here. + */ + n += mStorage.ShallowSizeOfExcludingThis(aMallocSizeOf); + for (auto iter = mStorage.Iter(); !iter.Done(); iter.Next()) { + auto& h = iter.Data(); + n += h->SizeOfIncludingThis(aMallocSizeOf); + } + if (mSingleStore != nullptr) { + // base::Histogram doesn't have SizeOfExcludingThis, so we are overcounting + // the pointer here. + n += mSingleStore->SizeOfIncludingThis(aMallocSizeOf); + } + return n; +} + +} // namespace + +//////////////////////////////////////////////////////////////////////// +//////////////////////////////////////////////////////////////////////// +// +// PRIVATE: class KeyedHistogram and internal_ReflectKeyedHistogram + +namespace { + +nsresult internal_ReflectKeyedHistogram( + const KeyedHistogramSnapshotData& aSnapshot, const HistogramInfo& info, + JSContext* aCx, JS::Handle<JSObject*> aObj) { + for (auto iter = aSnapshot.ConstIter(); !iter.Done(); iter.Next()) { + HistogramSnapshotData& keyData = iter.Data(); + + JS::RootedObject histogramSnapshot(aCx, JS_NewPlainObject(aCx)); + if (!histogramSnapshot) { + return NS_ERROR_FAILURE; + } + + if (NS_FAILED(internal_ReflectHistogramAndSamples(aCx, histogramSnapshot, + info, keyData))) { + return NS_ERROR_FAILURE; + } + + const NS_ConvertUTF8toUTF16 key(iter.Key()); + if (!JS_DefineUCProperty(aCx, aObj, key.Data(), key.Length(), + histogramSnapshot, JSPROP_ENUMERATE)) { + return NS_ERROR_FAILURE; + } + } + + return NS_OK; +} + +KeyedHistogram::KeyedHistogram(HistogramID id, const HistogramInfo& info, + bool expired) + : mStorage(), + mSingleStore(nullptr), + mId(id), + mHistogramInfo(info), + mIsExpired(expired) { + if (IsExpired()) { + return; + } + + if (info.is_single_store()) { + mSingleStore = new KeyedHistogramMapType; + } else { + for (uint32_t i = 0; i < info.store_count; i++) { + auto store = nsDependentCString( + &gHistogramStringTable[gHistogramStoresTable[info.store_index + i]]); + mStorage.Put(store, new KeyedHistogramMapType); + } + } +} + +KeyedHistogram::~KeyedHistogram() { delete mSingleStore; } + +nsresult KeyedHistogram::GetHistogram(const nsCString& aStore, + const nsCString& key, + base::Histogram** histogram) { + if (IsExpired()) { + MOZ_ASSERT(false, + "KeyedHistogram::GetHistogram called on an expired histogram."); + return NS_ERROR_FAILURE; + } + + KeyedHistogramMapType* histogramMap; + bool found; + + if (mSingleStore != nullptr) { + histogramMap = mSingleStore; + } else { + found = mStorage.Get(aStore, &histogramMap); + if (!found) { + return NS_ERROR_FAILURE; + } + } + + found = histogramMap->Get(key, histogram); + if (found) { + return NS_OK; + } + + int bucketsOffset = gHistogramBucketLowerBoundIndex[mId]; + base::Histogram* h = + internal_CreateBaseHistogramInstance(mHistogramInfo, bucketsOffset); + if (!h) { + return NS_ERROR_FAILURE; + } + + h->ClearFlags(base::Histogram::kUmaTargetedHistogramFlag); + *histogram = h; + + bool inserted = histogramMap->Put(key, h, mozilla::fallible); + if (MOZ_UNLIKELY(!inserted)) { + return NS_ERROR_OUT_OF_MEMORY; + } + return NS_OK; +} + +base::Histogram* KeyedHistogram::GetHistogram(const nsCString& aStore, + const nsCString& key) { + base::Histogram* h = nullptr; + if (NS_FAILED(GetHistogram(aStore, key, &h))) { + return nullptr; + } + return h; +} + +nsresult KeyedHistogram::Add(const nsCString& key, uint32_t sample, + ProcessID aProcessType) { + MOZ_ASSERT(XRE_IsParentProcess(), + "Only add to keyed histograms in the parent process"); + if (!XRE_IsParentProcess()) { + return NS_ERROR_FAILURE; + } + + bool canRecordDataset = + CanRecordDataset(mHistogramInfo.dataset, internal_CanRecordBase(), + internal_CanRecordExtended()); + // If `histogram` is a non-parent-process histogram, then recording-enabled + // has been checked in its owner process. + if (!canRecordDataset || (aProcessType == ProcessID::Parent && + !internal_IsRecordingEnabled(mId))) { + return NS_OK; + } + + // Don't record if expired. + if (IsExpired()) { + return NS_OK; + } + + // Don't record if the current platform is not enabled + if (!CanRecordProduct(gHistogramInfos[mId].products)) { + return NS_OK; + } + + // The internal representation of a base::Histogram's buckets uses `int`. + // Clamp large values of `sample` to be INT_MAX so they continue to be treated + // as large values (instead of negative ones). + if (sample > INT_MAX) { + TelemetryScalar::Add( + mozilla::Telemetry::ScalarID::TELEMETRY_ACCUMULATE_CLAMPED_VALUES, + NS_ConvertASCIItoUTF16(mHistogramInfo.name()), 1); + sample = INT_MAX; + } + + base::Histogram* histogram; + if (mSingleStore != nullptr) { + histogram = GetHistogram("main"_ns, key); + if (!histogram) { + MOZ_ASSERT(false, "Missing histogram in single store."); + return NS_ERROR_FAILURE; + } + + histogram->Add(sample); + } else { + for (uint32_t i = 0; i < mHistogramInfo.store_count; i++) { + auto store = nsDependentCString( + &gHistogramStringTable + [gHistogramStoresTable[mHistogramInfo.store_index + i]]); + base::Histogram* histogram = GetHistogram(store, key); + MOZ_ASSERT(histogram); + if (histogram) { + histogram->Add(sample); + } else { + return NS_ERROR_FAILURE; + } + } + } + + return NS_OK; +} + +void KeyedHistogram::Clear(const nsACString& aStore) { + MOZ_ASSERT(XRE_IsParentProcess(), + "Only clear keyed histograms in the parent process"); + if (!XRE_IsParentProcess()) { + return; + } + + if (IsExpired()) { + return; + } + + if (mSingleStore) { + if (aStore.EqualsASCII("main")) { + mSingleStore->Clear(); + } + return; + } + + KeyedHistogramMapType* histogramMap; + bool found = mStorage.Get(aStore, &histogramMap); + if (!found) { + return; + } + + histogramMap->Clear(); +} + +bool KeyedHistogram::IsEmpty(const nsACString& aStore) const { + if (mSingleStore != nullptr) { + if (aStore.EqualsASCII("main")) { + return mSingleStore->IsEmpty(); + } + + return true; + } + + KeyedHistogramMapType* histogramMap; + bool found = mStorage.Get(aStore, &histogramMap); + if (!found) { + return true; + } + return histogramMap->IsEmpty(); +} + +size_t KeyedHistogram::SizeOfIncludingThis( + mozilla::MallocSizeOf aMallocSizeOf) { + size_t n = 0; + n += aMallocSizeOf(this); + /* + * In theory mStorage.SizeOfExcludingThis should included the data part of the + * map, but the numbers seemed low, so we are only taking the shallow size and + * do the iteration here. + */ + n += mStorage.ShallowSizeOfExcludingThis(aMallocSizeOf); + for (auto iter = mStorage.Iter(); !iter.Done(); iter.Next()) { + auto& h = iter.Data(); + n += h->SizeOfIncludingThis(aMallocSizeOf); + } + if (mSingleStore != nullptr) { + // base::Histogram doesn't have SizeOfExcludingThis, so we are overcounting + // the pointer here. + n += mSingleStore->SizeOfExcludingThis(aMallocSizeOf); + } + return n; +} + +nsresult KeyedHistogram::GetKeys(const StaticMutexAutoLock& aLock, + const nsCString& store, + nsTArray<nsCString>& aKeys) { + KeyedHistogramMapType* histogramMap; + if (mSingleStore != nullptr) { + histogramMap = mSingleStore; + } else { + bool found = mStorage.Get(store, &histogramMap); + if (!found) { + return NS_ERROR_FAILURE; + } + } + + if (!aKeys.SetCapacity(histogramMap->Count(), mozilla::fallible)) { + return NS_ERROR_OUT_OF_MEMORY; + } + + for (auto iter = histogramMap->Iter(); !iter.Done(); iter.Next()) { + if (!aKeys.AppendElement(iter.Key(), mozilla::fallible)) { + return NS_ERROR_OUT_OF_MEMORY; + } + } + + return NS_OK; +} + +nsresult KeyedHistogram::GetJSSnapshot(JSContext* cx, JS::Handle<JSObject*> obj, + const nsACString& aStore, + bool clearSubsession) { + // Get a snapshot of the data. + KeyedHistogramSnapshotData dataSnapshot; + { + StaticMutexAutoLock locker(gTelemetryHistogramMutex); + MOZ_ASSERT(internal_IsHistogramEnumId(mId)); + + // Take a snapshot of the data here, protected by the lock, and then, + // outside of the lock protection, mirror it to a JS structure. + nsresult rv = GetSnapshot(locker, aStore, dataSnapshot, clearSubsession); + if (NS_FAILED(rv)) { + return rv; + } + } + + // Now that we have a copy of the data, mirror it to JS. + return internal_ReflectKeyedHistogram(dataSnapshot, gHistogramInfos[mId], cx, + obj); +} + +/** + * Return a histogram snapshot for the named store. + * + * If getting the snapshot succeeds, NS_OK is returned and `aSnapshot` contains + * the snapshot data. If the histogram is not available in the named store, + * NS_ERROR_NO_CONTENT is returned. For other errors, NS_ERROR_FAILURE is + * returned. + */ +nsresult KeyedHistogram::GetSnapshot(const StaticMutexAutoLock& aLock, + const nsACString& aStore, + KeyedHistogramSnapshotData& aSnapshot, + bool aClearSubsession) { + KeyedHistogramMapType* histogramMap; + if (mSingleStore != nullptr) { + if (!aStore.EqualsASCII("main")) { + return NS_ERROR_NO_CONTENT; + } + + histogramMap = mSingleStore; + } else { + bool found = mStorage.Get(aStore, &histogramMap); + if (!found) { + // Nothing in the main store is fine, it's just handled as empty + return NS_ERROR_NO_CONTENT; + } + } + + // Snapshot every key. + for (auto iter = histogramMap->ConstIter(); !iter.Done(); iter.Next()) { + base::Histogram* keyData = iter.UserData(); + if (!keyData) { + return NS_ERROR_FAILURE; + } + + HistogramSnapshotData keySnapshot; + if (NS_FAILED( + internal_GetHistogramAndSamples(aLock, keyData, keySnapshot))) { + return NS_ERROR_FAILURE; + } + + // Append to the final snapshot. + aSnapshot.Put(iter.Key(), std::move(keySnapshot)); + } + + if (aClearSubsession) { + Clear(aStore); + } + + return NS_OK; +} + +/** + * Helper function to get a snapshot of the keyed histograms. + * + * @param {aLock} the lock proof. + * @param {aDataset} the dataset for which the snapshot is being requested. + * @param {aClearSubsession} whether or not to clear the data after + * taking the snapshot. + * @param {aIncludeGPU} whether or not to include data for the GPU. + * @param {aOutSnapshot} the container in which the snapshot data will be + * stored. + * @return {nsresult} NS_OK if the snapshot was successfully taken or + * NS_ERROR_OUT_OF_MEMORY if it failed to allocate memory. + */ +nsresult internal_GetKeyedHistogramsSnapshot( + const StaticMutexAutoLock& aLock, const nsACString& aStore, + unsigned int aDataset, bool aClearSubsession, bool aIncludeGPU, + bool aFilterTest, KeyedHistogramProcessSnapshotsArray& aOutSnapshot) { + if (!aOutSnapshot.resize(static_cast<uint32_t>(ProcessID::Count))) { + return NS_ERROR_OUT_OF_MEMORY; + } + + for (uint32_t process = 0; process < static_cast<uint32_t>(ProcessID::Count); + ++process) { + KeyedHistogramSnapshotsArray& hArray = aOutSnapshot[process]; + + for (size_t i = 0; i < HistogramCount; ++i) { + HistogramID id = HistogramID(i); + const HistogramInfo& info = gHistogramInfos[id]; + if (!info.keyed) { + continue; + } + + if (!CanRecordInProcess(info.record_in_processes, ProcessID(process)) || + ((ProcessID(process) == ProcessID::Gpu) && !aIncludeGPU)) { + continue; + } + + if (!IsInDataset(info.dataset, aDataset)) { + continue; + } + + KeyedHistogram* keyed = + internal_GetKeyedHistogramById(id, ProcessID(process), + /* instantiate = */ false); + if (!keyed || keyed->IsEmpty(aStore) || keyed->IsExpired()) { + continue; + } + + const char* name = info.name(); + if (aFilterTest && strncmp(TEST_HISTOGRAM_PREFIX, name, + strlen(TEST_HISTOGRAM_PREFIX)) == 0) { + if (aClearSubsession) { + keyed->Clear(aStore); + } + continue; + } + + // Take a snapshot of the keyed histogram data! + KeyedHistogramSnapshotData snapshot; + if (!NS_SUCCEEDED( + keyed->GetSnapshot(aLock, aStore, snapshot, aClearSubsession))) { + return NS_ERROR_FAILURE; + } + + if (!hArray.emplaceBack( + KeyedHistogramSnapshotInfo{std::move(snapshot), id})) { + return NS_ERROR_OUT_OF_MEMORY; + } + } + } + return NS_OK; +} + +} // namespace + +//////////////////////////////////////////////////////////////////////// +//////////////////////////////////////////////////////////////////////// +// +// PRIVATE: thread-unsafe helpers for the external interface + +namespace { + +bool internal_RemoteAccumulate(const StaticMutexAutoLock& aLock, + HistogramID aId, uint32_t aSample) { + if (XRE_IsParentProcess()) { + return false; + } + + if (!internal_IsRecordingEnabled(aId)) { + return true; + } + + TelemetryIPCAccumulator::AccumulateChildHistogram(aId, aSample); + return true; +} + +bool internal_RemoteAccumulate(const StaticMutexAutoLock& aLock, + HistogramID aId, const nsCString& aKey, + uint32_t aSample) { + if (XRE_IsParentProcess()) { + return false; + } + + if (!internal_IsRecordingEnabled(aId)) { + return true; + } + + TelemetryIPCAccumulator::AccumulateChildKeyedHistogram(aId, aKey, aSample); + return true; +} + +void internal_Accumulate(const StaticMutexAutoLock& aLock, HistogramID aId, + uint32_t aSample) { + if (!internal_CanRecordBase() || + internal_RemoteAccumulate(aLock, aId, aSample)) { + return; + } + + Histogram* w = internal_GetHistogramById(aLock, aId, ProcessID::Parent); + MOZ_ASSERT(w); + internal_HistogramAdd(aLock, *w, aId, aSample, ProcessID::Parent); +} + +void internal_Accumulate(const StaticMutexAutoLock& aLock, HistogramID aId, + const nsCString& aKey, uint32_t aSample) { + if (!gInitDone || !internal_CanRecordBase() || + internal_RemoteAccumulate(aLock, aId, aKey, aSample)) { + return; + } + + KeyedHistogram* keyed = + internal_GetKeyedHistogramById(aId, ProcessID::Parent); + MOZ_ASSERT(keyed); + keyed->Add(aKey, aSample, ProcessID::Parent); +} + +void internal_AccumulateChild(const StaticMutexAutoLock& aLock, + ProcessID aProcessType, HistogramID aId, + uint32_t aSample) { + if (!internal_CanRecordBase()) { + return; + } + + Histogram* w = internal_GetHistogramById(aLock, aId, aProcessType); + if (w == nullptr) { + NS_WARNING("Failed GetHistogramById for CHILD"); + } else { + internal_HistogramAdd(aLock, *w, aId, aSample, aProcessType); + } +} + +void internal_AccumulateChildKeyed(const StaticMutexAutoLock& aLock, + ProcessID aProcessType, HistogramID aId, + const nsCString& aKey, uint32_t aSample) { + if (!gInitDone || !internal_CanRecordBase()) { + return; + } + + KeyedHistogram* keyed = internal_GetKeyedHistogramById(aId, aProcessType); + MOZ_ASSERT(keyed); + keyed->Add(aKey, aSample, aProcessType); +} + +void internal_ClearHistogram(const StaticMutexAutoLock& aLock, HistogramID id, + const nsACString& aStore) { + MOZ_ASSERT(XRE_IsParentProcess()); + if (!XRE_IsParentProcess()) { + return; + } + + // Handle keyed histograms. + if (gHistogramInfos[id].keyed) { + for (uint32_t process = 0; + process < static_cast<uint32_t>(ProcessID::Count); ++process) { + KeyedHistogram* kh = internal_GetKeyedHistogramById( + id, static_cast<ProcessID>(process), /* instantiate = */ false); + if (kh) { + kh->Clear(aStore); + } + } + } else { + // Reset the histograms instances for all processes. + for (uint32_t process = 0; + process < static_cast<uint32_t>(ProcessID::Count); ++process) { + Histogram* h = + internal_GetHistogramById(aLock, id, static_cast<ProcessID>(process), + /* instantiate = */ false); + if (h) { + h->Clear(aStore); + } + } + } +} + +} // namespace + +//////////////////////////////////////////////////////////////////////// +//////////////////////////////////////////////////////////////////////// +// +// PRIVATE: JSHistogram_* functions + +// NOTE: the functions in this section: +// +// internal_JSHistogram_Add +// internal_JSHistogram_Name +// internal_JSHistogram_Snapshot +// internal_JSHistogram_Clear +// internal_WrapAndReturnHistogram +// +// all run without protection from |gTelemetryHistogramMutex|. If they +// held |gTelemetryHistogramMutex|, there would be the possibility of +// deadlock because the JS_ calls that they make may call back into the +// TelemetryHistogram interface, hence trying to re-acquire the mutex. +// +// This means that these functions potentially race against threads, but +// that seems preferable to risking deadlock. + +namespace { + +void internal_JSHistogram_finalize(JSFreeOp*, JSObject*); + +static const JSClassOps sJSHistogramClassOps = {nullptr, /* addProperty */ + nullptr, /* delProperty */ + nullptr, /* enumerate */ + nullptr, /* newEnumerate */ + nullptr, /* resolve */ + nullptr, /* mayResolve */ + internal_JSHistogram_finalize}; + +static const JSClass sJSHistogramClass = { + "JSHistogram", /* name */ + JSCLASS_HAS_PRIVATE | JSCLASS_FOREGROUND_FINALIZE, /* flags */ + &sJSHistogramClassOps}; + +struct JSHistogramData { + HistogramID histogramId; +}; + +bool internal_JSHistogram_CoerceValue(JSContext* aCx, + JS::Handle<JS::Value> aElement, + HistogramID aId, uint32_t aHistogramType, + uint32_t& aValue) { + if (aElement.isString()) { + // Strings only allowed for categorical histograms + if (aHistogramType != nsITelemetry::HISTOGRAM_CATEGORICAL) { + LogToBrowserConsole( + nsIScriptError::errorFlag, + nsLiteralString( + u"String argument only allowed for categorical histogram")); + return false; + } + + // Label is given by the string argument + nsAutoJSString label; + if (!label.init(aCx, aElement)) { + LogToBrowserConsole(nsIScriptError::errorFlag, + u"Invalid string parameter"_ns); + return false; + } + + // Get the label id for accumulation + nsresult rv = gHistogramInfos[aId].label_id( + NS_ConvertUTF16toUTF8(label).get(), &aValue); + if (NS_FAILED(rv)) { + nsPrintfCString msg("'%s' is an invalid string label", + NS_ConvertUTF16toUTF8(label).get()); + LogToBrowserConsole(nsIScriptError::errorFlag, + NS_ConvertUTF8toUTF16(msg)); + return false; + } + } else if (!(aElement.isNumber() || aElement.isBoolean())) { + LogToBrowserConsole(nsIScriptError::errorFlag, u"Argument not a number"_ns); + return false; + } else if (aElement.isNumber() && aElement.toNumber() > UINT32_MAX) { + // Clamp large numerical arguments to aValue's acceptable values. + // JS::ToUint32 will take aElement modulo 2^32 before returning it, which + // may result in a smaller final value. + aValue = UINT32_MAX; +#ifdef DEBUG + LogToBrowserConsole(nsIScriptError::errorFlag, + u"Clamped large numeric value"_ns); +#endif + } else if (!JS::ToUint32(aCx, aElement, &aValue)) { + LogToBrowserConsole(nsIScriptError::errorFlag, + u"Failed to convert element to UInt32"_ns); + return false; + } + + // If we're here then all type checks have passed and aValue contains the + // coerced integer + return true; +} + +bool internal_JSHistogram_GetValueArray(JSContext* aCx, JS::CallArgs& args, + uint32_t aHistogramType, + HistogramID aId, bool isKeyed, + nsTArray<uint32_t>& aArray) { + // This function populates aArray with the values extracted from args. Handles + // keyed and non-keyed histograms, and single and array of values. Also + // performs sanity checks on the arguments. Returns true upon successful + // population, false otherwise. + + uint32_t firstArgIndex = 0; + if (isKeyed) { + firstArgIndex = 1; + } + + // Special case of no argument (or only key) and count histogram + if (args.length() == firstArgIndex) { + if (!(aHistogramType == nsITelemetry::HISTOGRAM_COUNT)) { + LogToBrowserConsole( + nsIScriptError::errorFlag, + nsLiteralString( + u"Need at least one argument for non count type histogram")); + return false; + } + + aArray.AppendElement(1); + return true; + } + + if (args[firstArgIndex].isObject() && !args[firstArgIndex].isString()) { + JS::Rooted<JSObject*> arrayObj(aCx, &args[firstArgIndex].toObject()); + + bool isArray = false; + JS::IsArrayObject(aCx, arrayObj, &isArray); + + if (!isArray) { + LogToBrowserConsole( + nsIScriptError::errorFlag, + nsLiteralString( + u"The argument to accumulate can't be a non-array object")); + return false; + } + + uint32_t arrayLength = 0; + if (!JS::GetArrayLength(aCx, arrayObj, &arrayLength)) { + LogToBrowserConsole(nsIScriptError::errorFlag, + u"Failed while trying to get array length"_ns); + return false; + } + + for (uint32_t arrayIdx = 0; arrayIdx < arrayLength; arrayIdx++) { + JS::Rooted<JS::Value> element(aCx); + + if (!JS_GetElement(aCx, arrayObj, arrayIdx, &element)) { + nsPrintfCString msg("Failed while trying to get element at index %d", + arrayIdx); + LogToBrowserConsole(nsIScriptError::errorFlag, + NS_ConvertUTF8toUTF16(msg)); + return false; + } + + uint32_t value = 0; + if (!internal_JSHistogram_CoerceValue(aCx, element, aId, aHistogramType, + value)) { + nsPrintfCString msg("Element at index %d failed type checks", arrayIdx); + LogToBrowserConsole(nsIScriptError::errorFlag, + NS_ConvertUTF8toUTF16(msg)); + return false; + } + aArray.AppendElement(value); + } + + return true; + } + + uint32_t value = 0; + if (!internal_JSHistogram_CoerceValue(aCx, args[firstArgIndex], aId, + aHistogramType, value)) { + return false; + } + aArray.AppendElement(value); + return true; +} + +bool internal_JSHistogram_Add(JSContext* cx, unsigned argc, JS::Value* vp) { + JS::CallArgs args = CallArgsFromVp(argc, vp); + + if (!args.thisv().isObject() || + JS::GetClass(&args.thisv().toObject()) != &sJSHistogramClass) { + JS_ReportErrorASCII(cx, "Wrong JS class, expected JSHistogram class"); + return false; + } + + JSObject* obj = &args.thisv().toObject(); + JSHistogramData* data = static_cast<JSHistogramData*>(JS::GetPrivate(obj)); + MOZ_ASSERT(data); + HistogramID id = data->histogramId; + MOZ_ASSERT(internal_IsHistogramEnumId(id)); + uint32_t type = gHistogramInfos[id].histogramType; + + // This function should always return |undefined| and never fail but + // rather report failures using the console. + args.rval().setUndefined(); + + nsTArray<uint32_t> values; + if (!internal_JSHistogram_GetValueArray(cx, args, type, id, false, values)) { + // Either GetValueArray or CoerceValue utility function will have printed a + // meaningful error message, so we simply return true + return true; + } + + { + StaticMutexAutoLock locker(gTelemetryHistogramMutex); + for (uint32_t aValue : values) { + internal_Accumulate(locker, id, aValue); + } + } + return true; +} + +bool internal_JSHistogram_Name(JSContext* cx, unsigned argc, JS::Value* vp) { + JS::CallArgs args = CallArgsFromVp(argc, vp); + + if (!args.thisv().isObject() || + JS::GetClass(&args.thisv().toObject()) != &sJSHistogramClass) { + JS_ReportErrorASCII(cx, "Wrong JS class, expected JSHistogram class"); + return false; + } + + JSObject* obj = &args.thisv().toObject(); + JSHistogramData* data = static_cast<JSHistogramData*>(JS::GetPrivate(obj)); + MOZ_ASSERT(data); + HistogramID id = data->histogramId; + MOZ_ASSERT(internal_IsHistogramEnumId(id)); + const char* name = gHistogramInfos[id].name(); + + auto cname = NS_ConvertASCIItoUTF16(name); + args.rval().setString(ToJSString(cx, cname)); + + return true; +} + +/** + * Extract the store name from JavaScript function arguments. + * The first and only argument needs to be an object with a "store" property. + * If no arguments are given it defaults to "main". + */ +nsresult internal_JS_StoreFromObjectArgument(JSContext* cx, + const JS::CallArgs& args, + nsAutoString& aStoreName) { + if (args.length() == 0) { + aStoreName.AssignLiteral("main"); + } else if (args.length() == 1) { + if (!args[0].isObject()) { + JS_ReportErrorASCII(cx, "Expected object argument."); + return NS_ERROR_FAILURE; + } + + JS::RootedValue storeValue(cx); + JS::RootedObject argsObject(cx, &args[0].toObject()); + if (!JS_GetProperty(cx, argsObject, "store", &storeValue)) { + JS_ReportErrorASCII(cx, + "Expected object argument to have property 'store'."); + return NS_ERROR_FAILURE; + } + + nsAutoJSString store; + if (!storeValue.isString() || !store.init(cx, storeValue)) { + JS_ReportErrorASCII( + cx, "Expected object argument's 'store' property to be a string."); + return NS_ERROR_FAILURE; + } + + aStoreName.Assign(store); + } else { + JS_ReportErrorASCII(cx, "Expected at most one argument."); + return NS_ERROR_FAILURE; + } + + return NS_OK; +} + +bool internal_JSHistogram_Snapshot(JSContext* cx, unsigned argc, + JS::Value* vp) { + JS::CallArgs args = JS::CallArgsFromVp(argc, vp); + + if (!XRE_IsParentProcess()) { + JS_ReportErrorASCII( + cx, "Histograms can only be snapshotted in the parent process"); + return false; + } + + if (!args.thisv().isObject() || + JS::GetClass(&args.thisv().toObject()) != &sJSHistogramClass) { + JS_ReportErrorASCII(cx, "Wrong JS class, expected JSHistogram class"); + return false; + } + + JSObject* obj = &args.thisv().toObject(); + JSHistogramData* data = static_cast<JSHistogramData*>(JS::GetPrivate(obj)); + MOZ_ASSERT(data); + HistogramID id = data->histogramId; + + nsAutoString storeName; + nsresult rv = internal_JS_StoreFromObjectArgument(cx, args, storeName); + if (NS_FAILED(rv)) { + return false; + } + + HistogramSnapshotData dataSnapshot; + { + StaticMutexAutoLock locker(gTelemetryHistogramMutex); + MOZ_ASSERT(internal_IsHistogramEnumId(id)); + + // This is not good standard behavior given that we have histogram instances + // covering multiple processes. + // However, changing this requires some broader changes to callers. + Histogram* w = internal_GetHistogramById(locker, id, ProcessID::Parent); + base::Histogram* h = nullptr; + if (!w->GetHistogram(NS_ConvertUTF16toUTF8(storeName), &h)) { + // When it's not in the named store, let's skip the snapshot completely, + // but don't fail + args.rval().setUndefined(); + return true; + } + // Take a snapshot of the data here, protected by the lock, and then, + // outside of the lock protection, mirror it to a JS structure + if (NS_FAILED(internal_GetHistogramAndSamples(locker, h, dataSnapshot))) { + return false; + } + } + + JS::Rooted<JSObject*> snapshot(cx, JS_NewPlainObject(cx)); + if (!snapshot) { + return false; + } + + if (NS_FAILED(internal_ReflectHistogramAndSamples( + cx, snapshot, gHistogramInfos[id], dataSnapshot))) { + return false; + } + + args.rval().setObject(*snapshot); + return true; +} + +bool internal_JSHistogram_Clear(JSContext* cx, unsigned argc, JS::Value* vp) { + if (!XRE_IsParentProcess()) { + JS_ReportErrorASCII(cx, + "Histograms can only be cleared in the parent process"); + return false; + } + + JS::CallArgs args = JS::CallArgsFromVp(argc, vp); + + if (!args.thisv().isObject() || + JS::GetClass(&args.thisv().toObject()) != &sJSHistogramClass) { + JS_ReportErrorASCII(cx, "Wrong JS class, expected JSHistogram class"); + return false; + } + + JSObject* obj = &args.thisv().toObject(); + JSHistogramData* data = static_cast<JSHistogramData*>(JS::GetPrivate(obj)); + MOZ_ASSERT(data); + + nsAutoString storeName; + nsresult rv = internal_JS_StoreFromObjectArgument(cx, args, storeName); + if (NS_FAILED(rv)) { + return false; + } + + // This function should always return |undefined| and never fail but + // rather report failures using the console. + args.rval().setUndefined(); + + HistogramID id = data->histogramId; + { + StaticMutexAutoLock locker(gTelemetryHistogramMutex); + + MOZ_ASSERT(internal_IsHistogramEnumId(id)); + internal_ClearHistogram(locker, id, NS_ConvertUTF16toUTF8(storeName)); + } + + return true; +} + +// NOTE: Runs without protection from |gTelemetryHistogramMutex|. +// See comment at the top of this section. +nsresult internal_WrapAndReturnHistogram(HistogramID id, JSContext* cx, + JS::MutableHandle<JS::Value> ret) { + JS::Rooted<JSObject*> obj(cx, JS_NewObject(cx, &sJSHistogramClass)); + if (!obj) { + return NS_ERROR_FAILURE; + } + + // The 3 functions that are wrapped up here are eventually called + // by the same thread that runs this function. + if (!(JS_DefineFunction(cx, obj, "add", internal_JSHistogram_Add, 1, 0) && + JS_DefineFunction(cx, obj, "name", internal_JSHistogram_Name, 1, 0) && + JS_DefineFunction(cx, obj, "snapshot", internal_JSHistogram_Snapshot, 1, + 0) && + JS_DefineFunction(cx, obj, "clear", internal_JSHistogram_Clear, 1, + 0))) { + return NS_ERROR_FAILURE; + } + + JSHistogramData* data = new JSHistogramData{id}; + JS::SetPrivate(obj, data); + ret.setObject(*obj); + + return NS_OK; +} + +void internal_JSHistogram_finalize(JSFreeOp*, JSObject* obj) { + if (!obj || JS::GetClass(obj) != &sJSHistogramClass) { + MOZ_ASSERT_UNREACHABLE("Should have the right JS class."); + return; + } + + JSHistogramData* data = static_cast<JSHistogramData*>(JS::GetPrivate(obj)); + MOZ_ASSERT(data); + delete data; +} + +} // namespace + +//////////////////////////////////////////////////////////////////////// +//////////////////////////////////////////////////////////////////////// +// +// PRIVATE: JSKeyedHistogram_* functions + +// NOTE: the functions in this section: +// +// internal_JSKeyedHistogram_Add +// internal_JSKeyedHistogram_Name +// internal_JSKeyedHistogram_Keys +// internal_JSKeyedHistogram_Snapshot +// internal_JSKeyedHistogram_Clear +// internal_WrapAndReturnKeyedHistogram +// +// Same comments as above, at the JSHistogram_* section, regarding +// deadlock avoidance, apply. + +namespace { + +void internal_JSKeyedHistogram_finalize(JSFreeOp*, JSObject*); + +static const JSClassOps sJSKeyedHistogramClassOps = { + nullptr, /* addProperty */ + nullptr, /* delProperty */ + nullptr, /* enumerate */ + nullptr, /* newEnumerate */ + nullptr, /* resolve */ + nullptr, /* mayResolve */ + internal_JSKeyedHistogram_finalize}; + +static const JSClass sJSKeyedHistogramClass = { + "JSKeyedHistogram", /* name */ + JSCLASS_HAS_PRIVATE | JSCLASS_FOREGROUND_FINALIZE, /* flags */ + &sJSKeyedHistogramClassOps}; + +bool internal_JSKeyedHistogram_Snapshot(JSContext* cx, unsigned argc, + JS::Value* vp) { + if (!XRE_IsParentProcess()) { + JS_ReportErrorASCII( + cx, "Keyed histograms can only be snapshotted in the parent process"); + return false; + } + + JS::CallArgs args = JS::CallArgsFromVp(argc, vp); + + if (!args.thisv().isObject() || + JS::GetClass(&args.thisv().toObject()) != &sJSKeyedHistogramClass) { + JS_ReportErrorASCII(cx, "Wrong JS class, expected JSKeyedHistogram class"); + return false; + } + + JSObject* obj = &args.thisv().toObject(); + JSHistogramData* data = static_cast<JSHistogramData*>(JS::GetPrivate(obj)); + MOZ_ASSERT(data); + HistogramID id = data->histogramId; + MOZ_ASSERT(internal_IsHistogramEnumId(id)); + + // This function should always return |undefined| and never fail but + // rather report failures using the console. + args.rval().setUndefined(); + + // This is not good standard behavior given that we have histogram instances + // covering multiple processes. + // However, changing this requires some broader changes to callers. + KeyedHistogram* keyed = internal_GetKeyedHistogramById( + id, ProcessID::Parent, /* instantiate = */ true); + if (!keyed) { + JS_ReportErrorASCII(cx, "Failed to look up keyed histogram"); + return false; + } + + nsAutoString storeName; + nsresult rv; + rv = internal_JS_StoreFromObjectArgument(cx, args, storeName); + if (NS_FAILED(rv)) { + return false; + } + + JS::RootedObject snapshot(cx, JS_NewPlainObject(cx)); + if (!snapshot) { + JS_ReportErrorASCII(cx, "Failed to create object"); + return false; + } + + rv = keyed->GetJSSnapshot(cx, snapshot, NS_ConvertUTF16toUTF8(storeName), + false); + + // If the store is not available, we return nothing and don't fail + if (rv == NS_ERROR_NO_CONTENT) { + args.rval().setUndefined(); + return true; + } + + if (!NS_SUCCEEDED(rv)) { + JS_ReportErrorASCII(cx, "Failed to reflect keyed histograms"); + return false; + } + + args.rval().setObject(*snapshot); + return true; +} + +bool internal_JSKeyedHistogram_Add(JSContext* cx, unsigned argc, + JS::Value* vp) { + JS::CallArgs args = JS::CallArgsFromVp(argc, vp); + + if (!args.thisv().isObject() || + JS::GetClass(&args.thisv().toObject()) != &sJSKeyedHistogramClass) { + JS_ReportErrorASCII(cx, "Wrong JS class, expected JSKeyedHistogram class"); + return false; + } + + JSObject* obj = &args.thisv().toObject(); + JSHistogramData* data = static_cast<JSHistogramData*>(JS::GetPrivate(obj)); + MOZ_ASSERT(data); + HistogramID id = data->histogramId; + MOZ_ASSERT(internal_IsHistogramEnumId(id)); + + // This function should always return |undefined| and never fail but + // rather report failures using the console. + args.rval().setUndefined(); + if (args.length() < 1) { + LogToBrowserConsole(nsIScriptError::errorFlag, u"Expected one argument"_ns); + return true; + } + + nsAutoJSString key; + if (!args[0].isString() || !key.init(cx, args[0])) { + LogToBrowserConsole(nsIScriptError::errorFlag, u"Not a string"_ns); + return true; + } + + // Check if we're allowed to record in the provided key, for this histogram. + if (!gHistogramInfos[id].allows_key(NS_ConvertUTF16toUTF8(key))) { + nsPrintfCString msg("%s - key '%s' not allowed for this keyed histogram", + gHistogramInfos[id].name(), + NS_ConvertUTF16toUTF8(key).get()); + LogToBrowserConsole(nsIScriptError::errorFlag, NS_ConvertUTF8toUTF16(msg)); + TelemetryScalar::Add(mozilla::Telemetry::ScalarID:: + TELEMETRY_ACCUMULATE_UNKNOWN_HISTOGRAM_KEYS, + NS_ConvertASCIItoUTF16(gHistogramInfos[id].name()), 1); + return true; + } + + const uint32_t type = gHistogramInfos[id].histogramType; + + nsTArray<uint32_t> values; + if (!internal_JSHistogram_GetValueArray(cx, args, type, id, true, values)) { + // Either GetValueArray or CoerceValue utility function will have printed a + // meaningful error message so we simple return true + return true; + } + + { + StaticMutexAutoLock locker(gTelemetryHistogramMutex); + for (uint32_t aValue : values) { + internal_Accumulate(locker, id, NS_ConvertUTF16toUTF8(key), aValue); + } + } + return true; +} + +bool internal_JSKeyedHistogram_Name(JSContext* cx, unsigned argc, + JS::Value* vp) { + JS::CallArgs args = CallArgsFromVp(argc, vp); + + if (!args.thisv().isObject() || + JS::GetClass(&args.thisv().toObject()) != &sJSKeyedHistogramClass) { + JS_ReportErrorASCII(cx, "Wrong JS class, expected JSKeyedHistogram class"); + return false; + } + + JSObject* obj = &args.thisv().toObject(); + JSHistogramData* data = static_cast<JSHistogramData*>(JS::GetPrivate(obj)); + MOZ_ASSERT(data); + HistogramID id = data->histogramId; + MOZ_ASSERT(internal_IsHistogramEnumId(id)); + const char* name = gHistogramInfos[id].name(); + + auto cname = NS_ConvertASCIItoUTF16(name); + args.rval().setString(ToJSString(cx, cname)); + + return true; +} + +bool internal_JSKeyedHistogram_Keys(JSContext* cx, unsigned argc, + JS::Value* vp) { + JS::CallArgs args = JS::CallArgsFromVp(argc, vp); + + if (!args.thisv().isObject() || + JS::GetClass(&args.thisv().toObject()) != &sJSKeyedHistogramClass) { + JS_ReportErrorASCII(cx, "Wrong JS class, expected JSKeyedHistogram class"); + return false; + } + + JSObject* obj = &args.thisv().toObject(); + JSHistogramData* data = static_cast<JSHistogramData*>(JS::GetPrivate(obj)); + MOZ_ASSERT(data); + HistogramID id = data->histogramId; + + nsAutoString storeName; + nsresult rv = internal_JS_StoreFromObjectArgument(cx, args, storeName); + if (NS_FAILED(rv)) { + return false; + } + + nsTArray<nsCString> keys; + { + StaticMutexAutoLock locker(gTelemetryHistogramMutex); + MOZ_ASSERT(internal_IsHistogramEnumId(id)); + + // This is not good standard behavior given that we have histogram instances + // covering multiple processes. + // However, changing this requires some broader changes to callers. + KeyedHistogram* keyed = + internal_GetKeyedHistogramById(id, ProcessID::Parent); + + MOZ_ASSERT(keyed); + if (!keyed) { + return false; + } + + if (NS_FAILED( + keyed->GetKeys(locker, NS_ConvertUTF16toUTF8(storeName), keys))) { + return false; + } + } + + // Convert keys from nsTArray<nsCString> to JS array. + JS::RootedVector<JS::Value> autoKeys(cx); + if (!autoKeys.reserve(keys.Length())) { + return false; + } + + for (const auto& key : keys) { + JS::RootedValue jsKey(cx); + jsKey.setString(ToJSString(cx, key)); + if (!autoKeys.append(jsKey)) { + return false; + } + } + + JS::RootedObject jsKeys(cx, JS::NewArrayObject(cx, autoKeys)); + if (!jsKeys) { + return false; + } + + args.rval().setObject(*jsKeys); + return true; +} + +bool internal_JSKeyedHistogram_Clear(JSContext* cx, unsigned argc, + JS::Value* vp) { + if (!XRE_IsParentProcess()) { + JS_ReportErrorASCII( + cx, "Keyed histograms can only be cleared in the parent process"); + return false; + } + + JS::CallArgs args = JS::CallArgsFromVp(argc, vp); + + if (!args.thisv().isObject() || + JS::GetClass(&args.thisv().toObject()) != &sJSKeyedHistogramClass) { + JS_ReportErrorASCII(cx, "Wrong JS class, expected JSKeyedHistogram class"); + return false; + } + + JSObject* obj = &args.thisv().toObject(); + JSHistogramData* data = static_cast<JSHistogramData*>(JS::GetPrivate(obj)); + MOZ_ASSERT(data); + HistogramID id = data->histogramId; + + // This function should always return |undefined| and never fail but + // rather report failures using the console. + args.rval().setUndefined(); + + nsAutoString storeName; + nsresult rv = internal_JS_StoreFromObjectArgument(cx, args, storeName); + if (NS_FAILED(rv)) { + return false; + } + + KeyedHistogram* keyed = nullptr; + { + MOZ_ASSERT(internal_IsHistogramEnumId(id)); + StaticMutexAutoLock locker(gTelemetryHistogramMutex); + + // This is not good standard behavior given that we have histogram instances + // covering multiple processes. + // However, changing this requires some broader changes to callers. + keyed = internal_GetKeyedHistogramById(id, ProcessID::Parent, + /* instantiate = */ false); + + if (!keyed) { + return true; + } + + keyed->Clear(NS_ConvertUTF16toUTF8(storeName)); + } + + return true; +} + +// NOTE: Runs without protection from |gTelemetryHistogramMutex|. +// See comment at the top of this section. +nsresult internal_WrapAndReturnKeyedHistogram( + HistogramID id, JSContext* cx, JS::MutableHandle<JS::Value> ret) { + JS::Rooted<JSObject*> obj(cx, JS_NewObject(cx, &sJSKeyedHistogramClass)); + if (!obj) return NS_ERROR_FAILURE; + // The 6 functions that are wrapped up here are eventually called + // by the same thread that runs this function. + if (!(JS_DefineFunction(cx, obj, "add", internal_JSKeyedHistogram_Add, 2, + 0) && + JS_DefineFunction(cx, obj, "name", internal_JSKeyedHistogram_Name, 1, + 0) && + JS_DefineFunction(cx, obj, "snapshot", + internal_JSKeyedHistogram_Snapshot, 1, 0) && + JS_DefineFunction(cx, obj, "keys", internal_JSKeyedHistogram_Keys, 1, + 0) && + JS_DefineFunction(cx, obj, "clear", internal_JSKeyedHistogram_Clear, 1, + 0))) { + return NS_ERROR_FAILURE; + } + + JSHistogramData* data = new JSHistogramData{id}; + JS::SetPrivate(obj, data); + ret.setObject(*obj); + + return NS_OK; +} + +void internal_JSKeyedHistogram_finalize(JSFreeOp*, JSObject* obj) { + if (!obj || JS::GetClass(obj) != &sJSKeyedHistogramClass) { + MOZ_ASSERT_UNREACHABLE("Should have the right JS class."); + return; + } + + JSHistogramData* data = static_cast<JSHistogramData*>(JS::GetPrivate(obj)); + MOZ_ASSERT(data); + delete data; +} + +} // namespace + +//////////////////////////////////////////////////////////////////////// +//////////////////////////////////////////////////////////////////////// +// +// EXTERNALLY VISIBLE FUNCTIONS in namespace TelemetryHistogram:: + +// All of these functions are actually in namespace TelemetryHistogram::, +// but the ::TelemetryHistogram prefix is given explicitly. This is +// because it is critical to see which calls from these functions are +// to another function in this interface. Mis-identifying "inwards +// calls" from "calls to another function in this interface" will lead +// to deadlocking and/or races. See comments at the top of the file +// for further (important!) details. + +void TelemetryHistogram::InitializeGlobalState(bool canRecordBase, + bool canRecordExtended) { + StaticMutexAutoLock locker(gTelemetryHistogramMutex); + MOZ_ASSERT(!gInitDone, + "TelemetryHistogram::InitializeGlobalState " + "may only be called once"); + + gCanRecordBase = canRecordBase; + gCanRecordExtended = canRecordExtended; + + if (XRE_IsParentProcess()) { + gHistogramStorage = + new Histogram* [HistogramCount * size_t(ProcessID::Count)] {}; + gKeyedHistogramStorage = + new KeyedHistogram* [HistogramCount * size_t(ProcessID::Count)] {}; + } + + // Some Telemetry histograms depend on the value of C++ constants and hardcode + // their values in Histograms.json. + // We add static asserts here for those values to match so that future changes + // don't go unnoticed. + // clang-format off + static_assert((uint32_t(JS::GCReason::NUM_TELEMETRY_REASONS) + 1) == + gHistogramInfos[mozilla::Telemetry::GC_MINOR_REASON].bucketCount && + (uint32_t(JS::GCReason::NUM_TELEMETRY_REASONS) + 1) == + gHistogramInfos[mozilla::Telemetry::GC_MINOR_REASON_LONG].bucketCount && + (uint32_t(JS::GCReason::NUM_TELEMETRY_REASONS) + 1) == + gHistogramInfos[mozilla::Telemetry::GC_REASON_2].bucketCount, + "NUM_TELEMETRY_REASONS is assumed to be a fixed value in Histograms.json." + " If this was an intentional change, update the n_values for the " + "following in Histograms.json: GC_MINOR_REASON, GC_MINOR_REASON_LONG, " + "GC_REASON_2"); + + // clang-format on + + gInitDone = true; +} + +void TelemetryHistogram::DeInitializeGlobalState() { + StaticMutexAutoLock locker(gTelemetryHistogramMutex); + gCanRecordBase = false; + gCanRecordExtended = false; + gInitDone = false; + + // FactoryGet `new`s Histograms for us, but requires us to manually delete. + if (XRE_IsParentProcess()) { + for (size_t i = 0; i < HistogramCount * size_t(ProcessID::Count); ++i) { + if (gKeyedHistogramStorage[i] != gExpiredKeyedHistogram) { + delete gKeyedHistogramStorage[i]; + } + if (gHistogramStorage[i] != gExpiredHistogram) { + delete gHistogramStorage[i]; + } + } + delete[] gHistogramStorage; + delete[] gKeyedHistogramStorage; + } + delete gExpiredHistogram; + gExpiredHistogram = nullptr; + delete gExpiredKeyedHistogram; + gExpiredKeyedHistogram = nullptr; +} + +#ifdef DEBUG +bool TelemetryHistogram::GlobalStateHasBeenInitialized() { + StaticMutexAutoLock locker(gTelemetryHistogramMutex); + return gInitDone; +} +#endif + +bool TelemetryHistogram::CanRecordBase() { + StaticMutexAutoLock locker(gTelemetryHistogramMutex); + return internal_CanRecordBase(); +} + +void TelemetryHistogram::SetCanRecordBase(bool b) { + StaticMutexAutoLock locker(gTelemetryHistogramMutex); + gCanRecordBase = b; +} + +bool TelemetryHistogram::CanRecordExtended() { + StaticMutexAutoLock locker(gTelemetryHistogramMutex); + return internal_CanRecordExtended(); +} + +void TelemetryHistogram::SetCanRecordExtended(bool b) { + StaticMutexAutoLock locker(gTelemetryHistogramMutex); + gCanRecordExtended = b; +} + +void TelemetryHistogram::InitHistogramRecordingEnabled() { + StaticMutexAutoLock locker(gTelemetryHistogramMutex); + auto processType = XRE_GetProcessType(); + for (size_t i = 0; i < HistogramCount; ++i) { + const HistogramInfo& h = gHistogramInfos[i]; + mozilla::Telemetry::HistogramID id = mozilla::Telemetry::HistogramID(i); + bool canRecordInProcess = + CanRecordInProcess(h.record_in_processes, processType); + internal_SetHistogramRecordingEnabled(locker, id, canRecordInProcess); + } + + for (auto recordingInitiallyDisabledID : kRecordingInitiallyDisabledIDs) { + internal_SetHistogramRecordingEnabled(locker, recordingInitiallyDisabledID, + false); + } +} + +void TelemetryHistogram::SetHistogramRecordingEnabled(HistogramID aID, + bool aEnabled) { + if (NS_WARN_IF(!internal_IsHistogramEnumId(aID))) { + MOZ_ASSERT_UNREACHABLE("Histogram usage requires valid ids."); + return; + } + + const HistogramInfo& h = gHistogramInfos[aID]; + if (!CanRecordInProcess(h.record_in_processes, XRE_GetProcessType())) { + // Don't permit record_in_process-disabled recording to be re-enabled. + return; + } + + if (!CanRecordProduct(h.products)) { + // Don't permit products-disabled recording to be re-enabled. + return; + } + + StaticMutexAutoLock locker(gTelemetryHistogramMutex); + internal_SetHistogramRecordingEnabled(locker, aID, aEnabled); +} + +nsresult TelemetryHistogram::SetHistogramRecordingEnabled( + const nsACString& name, bool aEnabled) { + StaticMutexAutoLock locker(gTelemetryHistogramMutex); + HistogramID id; + if (NS_FAILED(internal_GetHistogramIdByName(locker, name, &id))) { + return NS_ERROR_FAILURE; + } + + const HistogramInfo& hi = gHistogramInfos[id]; + if (CanRecordInProcess(hi.record_in_processes, XRE_GetProcessType())) { + internal_SetHistogramRecordingEnabled(locker, id, aEnabled); + } + return NS_OK; +} + +void TelemetryHistogram::Accumulate(HistogramID aID, uint32_t aSample) { + if (NS_WARN_IF(!internal_IsHistogramEnumId(aID))) { + MOZ_ASSERT_UNREACHABLE("Histogram usage requires valid ids."); + return; + } + + StaticMutexAutoLock locker(gTelemetryHistogramMutex); + internal_Accumulate(locker, aID, aSample); +} + +void TelemetryHistogram::Accumulate(HistogramID aID, + const nsTArray<uint32_t>& aSamples) { + if (NS_WARN_IF(!internal_IsHistogramEnumId(aID))) { + MOZ_ASSERT_UNREACHABLE("Histogram usage requires valid ids."); + return; + } + + MOZ_ASSERT(!gHistogramInfos[aID].keyed, + "Cannot accumulate into a keyed histogram. No key given."); + + StaticMutexAutoLock locker(gTelemetryHistogramMutex); + for (uint32_t sample : aSamples) { + internal_Accumulate(locker, aID, sample); + } +} + +void TelemetryHistogram::Accumulate(HistogramID aID, const nsCString& aKey, + uint32_t aSample) { + if (NS_WARN_IF(!internal_IsHistogramEnumId(aID))) { + MOZ_ASSERT_UNREACHABLE("Histogram usage requires valid ids."); + return; + } + + // Check if we're allowed to record in the provided key, for this histogram. + if (!gHistogramInfos[aID].allows_key(aKey)) { + nsPrintfCString msg("%s - key '%s' not allowed for this keyed histogram", + gHistogramInfos[aID].name(), aKey.get()); + LogToBrowserConsole(nsIScriptError::errorFlag, NS_ConvertUTF8toUTF16(msg)); + TelemetryScalar::Add(mozilla::Telemetry::ScalarID:: + TELEMETRY_ACCUMULATE_UNKNOWN_HISTOGRAM_KEYS, + NS_ConvertASCIItoUTF16(gHistogramInfos[aID].name()), + 1); + return; + } + + StaticMutexAutoLock locker(gTelemetryHistogramMutex); + internal_Accumulate(locker, aID, aKey, aSample); +} + +void TelemetryHistogram::Accumulate(HistogramID aID, const nsCString& aKey, + const nsTArray<uint32_t>& aSamples) { + if (NS_WARN_IF(!internal_IsHistogramEnumId(aID))) { + MOZ_ASSERT_UNREACHABLE("Histogram usage requires valid ids"); + return; + } + + // Check that this histogram is keyed + MOZ_ASSERT(gHistogramInfos[aID].keyed, + "Cannot accumulate into a non-keyed histogram using a key."); + + // Check if we're allowed to record in the provided key, for this histogram. + if (!gHistogramInfos[aID].allows_key(aKey)) { + nsPrintfCString msg("%s - key '%s' not allowed for this keyed histogram", + gHistogramInfos[aID].name(), aKey.get()); + LogToBrowserConsole(nsIScriptError::errorFlag, NS_ConvertUTF8toUTF16(msg)); + TelemetryScalar::Add(mozilla::Telemetry::ScalarID:: + TELEMETRY_ACCUMULATE_UNKNOWN_HISTOGRAM_KEYS, + NS_ConvertASCIItoUTF16(gHistogramInfos[aID].name()), + 1); + return; + } + + StaticMutexAutoLock locker(gTelemetryHistogramMutex); + for (uint32_t sample : aSamples) { + internal_Accumulate(locker, aID, aKey, sample); + } +} + +nsresult TelemetryHistogram::Accumulate(const char* name, uint32_t sample) { + StaticMutexAutoLock locker(gTelemetryHistogramMutex); + if (!internal_CanRecordBase()) { + return NS_ERROR_NOT_AVAILABLE; + } + HistogramID id; + nsresult rv = + internal_GetHistogramIdByName(locker, nsDependentCString(name), &id); + if (NS_FAILED(rv)) { + return rv; + } + internal_Accumulate(locker, id, sample); + return NS_OK; +} + +nsresult TelemetryHistogram::Accumulate(const char* name, const nsCString& key, + uint32_t sample) { + bool keyNotAllowed = false; + + { + StaticMutexAutoLock locker(gTelemetryHistogramMutex); + if (!internal_CanRecordBase()) { + return NS_ERROR_NOT_AVAILABLE; + } + HistogramID id; + nsresult rv = + internal_GetHistogramIdByName(locker, nsDependentCString(name), &id); + if (NS_SUCCEEDED(rv)) { + // Check if we're allowed to record in the provided key, for this + // histogram. + if (gHistogramInfos[id].allows_key(key)) { + internal_Accumulate(locker, id, key, sample); + return NS_OK; + } + // We're holding |gTelemetryHistogramMutex|, so we can't print a message + // here. + keyNotAllowed = true; + } + } + + if (keyNotAllowed) { + LogToBrowserConsole(nsIScriptError::errorFlag, + u"Key not allowed for this keyed histogram"_ns); + TelemetryScalar::Add(mozilla::Telemetry::ScalarID:: + TELEMETRY_ACCUMULATE_UNKNOWN_HISTOGRAM_KEYS, + NS_ConvertASCIItoUTF16(name), 1); + } + return NS_ERROR_FAILURE; +} + +void TelemetryHistogram::AccumulateCategorical(HistogramID aId, + const nsCString& label) { + if (NS_WARN_IF(!internal_IsHistogramEnumId(aId))) { + MOZ_ASSERT_UNREACHABLE("Histogram usage requires valid ids."); + return; + } + + StaticMutexAutoLock locker(gTelemetryHistogramMutex); + if (!internal_CanRecordBase()) { + return; + } + uint32_t labelId = 0; + if (NS_FAILED(gHistogramInfos[aId].label_id(label.get(), &labelId))) { + return; + } + internal_Accumulate(locker, aId, labelId); +} + +void TelemetryHistogram::AccumulateCategorical( + HistogramID aId, const nsTArray<nsCString>& aLabels) { + if (NS_WARN_IF(!internal_IsHistogramEnumId(aId))) { + MOZ_ASSERT_UNREACHABLE("Histogram usage requires valid ids."); + return; + } + + if (!internal_CanRecordBase()) { + return; + } + + // We use two loops, one for getting label_ids and another one for actually + // accumulating the values. This ensures that in the case of an invalid label + // in the array, no values are accumulated. In any call to this API, either + // all or (in case of error) none of the values will be accumulated. + + nsTArray<uint32_t> intSamples(aLabels.Length()); + for (const nsCString& label : aLabels) { + uint32_t labelId = 0; + if (NS_FAILED(gHistogramInfos[aId].label_id(label.get(), &labelId))) { + return; + } + intSamples.AppendElement(labelId); + } + + StaticMutexAutoLock locker(gTelemetryHistogramMutex); + + for (uint32_t sample : intSamples) { + internal_Accumulate(locker, aId, sample); + } +} + +void TelemetryHistogram::AccumulateChild( + ProcessID aProcessType, + const nsTArray<HistogramAccumulation>& aAccumulations) { + MOZ_ASSERT(XRE_IsParentProcess()); + + StaticMutexAutoLock locker(gTelemetryHistogramMutex); + if (!internal_CanRecordBase()) { + return; + } + for (uint32_t i = 0; i < aAccumulations.Length(); ++i) { + if (NS_WARN_IF(!internal_IsHistogramEnumId(aAccumulations[i].mId))) { + MOZ_ASSERT_UNREACHABLE("Histogram usage requires valid ids."); + continue; + } + internal_AccumulateChild(locker, aProcessType, aAccumulations[i].mId, + aAccumulations[i].mSample); + } +} + +void TelemetryHistogram::AccumulateChildKeyed( + ProcessID aProcessType, + const nsTArray<KeyedHistogramAccumulation>& aAccumulations) { + MOZ_ASSERT(XRE_IsParentProcess()); + StaticMutexAutoLock locker(gTelemetryHistogramMutex); + if (!internal_CanRecordBase()) { + return; + } + for (uint32_t i = 0; i < aAccumulations.Length(); ++i) { + if (NS_WARN_IF(!internal_IsHistogramEnumId(aAccumulations[i].mId))) { + MOZ_ASSERT_UNREACHABLE("Histogram usage requires valid ids."); + continue; + } + internal_AccumulateChildKeyed(locker, aProcessType, aAccumulations[i].mId, + aAccumulations[i].mKey, + aAccumulations[i].mSample); + } +} + +nsresult TelemetryHistogram::GetAllStores(StringHashSet& set) { + for (uint32_t storeIdx : gHistogramStoresTable) { + const char* name = &gHistogramStringTable[storeIdx]; + nsAutoCString store; + store.AssignASCII(name); + if (!set.PutEntry(store)) { + return NS_ERROR_FAILURE; + } + } + return NS_OK; +} + +nsresult TelemetryHistogram::GetCategoricalHistogramLabels( + JSContext* aCx, JS::MutableHandle<JS::Value> aResult) { + JS::Rooted<JSObject*> root_obj(aCx, JS_NewPlainObject(aCx)); + if (!root_obj) { + return NS_ERROR_FAILURE; + } + aResult.setObject(*root_obj); + + for (const HistogramInfo& info : gHistogramInfos) { + if (info.histogramType != nsITelemetry::HISTOGRAM_CATEGORICAL) { + continue; + } + + const char* name = info.name(); + JS::RootedObject labels(aCx, JS::NewArrayObject(aCx, info.label_count)); + if (!labels) { + return NS_ERROR_FAILURE; + } + + if (!JS_DefineProperty(aCx, root_obj, name, labels, JSPROP_ENUMERATE)) { + return NS_ERROR_FAILURE; + } + + for (uint32_t i = 0; i < info.label_count; ++i) { + uint32_t string_offset = gHistogramLabelTable[info.label_index + i]; + const char* const label = &gHistogramStringTable[string_offset]; + auto clabel = NS_ConvertASCIItoUTF16(label); + JS::Rooted<JS::Value> value(aCx); + value.setString(ToJSString(aCx, clabel)); + if (!JS_DefineElement(aCx, labels, i, value, JSPROP_ENUMERATE)) { + return NS_ERROR_FAILURE; + } + } + } + + return NS_OK; +} + +nsresult TelemetryHistogram::GetHistogramById( + const nsACString& name, JSContext* cx, JS::MutableHandle<JS::Value> ret) { + HistogramID id; + { + StaticMutexAutoLock locker(gTelemetryHistogramMutex); + nsresult rv = internal_GetHistogramIdByName(locker, name, &id); + if (NS_FAILED(rv)) { + return NS_ERROR_FAILURE; + } + + if (gHistogramInfos[id].keyed) { + return NS_ERROR_FAILURE; + } + } + // Runs without protection from |gTelemetryHistogramMutex| + return internal_WrapAndReturnHistogram(id, cx, ret); +} + +nsresult TelemetryHistogram::GetKeyedHistogramById( + const nsACString& name, JSContext* cx, JS::MutableHandle<JS::Value> ret) { + HistogramID id; + { + StaticMutexAutoLock locker(gTelemetryHistogramMutex); + nsresult rv = internal_GetHistogramIdByName(locker, name, &id); + if (NS_FAILED(rv)) { + return NS_ERROR_FAILURE; + } + + if (!gHistogramInfos[id].keyed) { + return NS_ERROR_FAILURE; + } + } + // Runs without protection from |gTelemetryHistogramMutex| + return internal_WrapAndReturnKeyedHistogram(id, cx, ret); +} + +const char* TelemetryHistogram::GetHistogramName(HistogramID id) { + if (NS_WARN_IF(!internal_IsHistogramEnumId(id))) { + MOZ_ASSERT_UNREACHABLE("Histogram usage requires valid ids."); + return nullptr; + } + + StaticMutexAutoLock locker(gTelemetryHistogramMutex); + const HistogramInfo& h = gHistogramInfos[id]; + return h.name(); +} + +nsresult TelemetryHistogram::CreateHistogramSnapshots( + JSContext* aCx, JS::MutableHandleValue aResult, const nsACString& aStore, + unsigned int aDataset, bool aClearSubsession, bool aFilterTest) { + if (!XRE_IsParentProcess()) { + return NS_ERROR_FAILURE; + } + + // Runs without protection from |gTelemetryHistogramMutex| + JS::Rooted<JSObject*> root_obj(aCx, JS_NewPlainObject(aCx)); + if (!root_obj) { + return NS_ERROR_FAILURE; + } + aResult.setObject(*root_obj); + + // Include the GPU process in histogram snapshots only if we actually tried + // to launch a process for it. + bool includeGPUProcess = internal_AttemptedGPUProcess(); + + HistogramProcessSnapshotsArray processHistArray; + { + StaticMutexAutoLock locker(gTelemetryHistogramMutex); + nsresult rv = internal_GetHistogramsSnapshot( + locker, aStore, aDataset, aClearSubsession, includeGPUProcess, + aFilterTest, processHistArray); + if (NS_FAILED(rv)) { + return rv; + } + } + + // Make the JS calls on the stashed histograms for every process + for (uint32_t process = 0; process < processHistArray.length(); ++process) { + JS::Rooted<JSObject*> processObject(aCx, JS_NewPlainObject(aCx)); + if (!processObject) { + return NS_ERROR_FAILURE; + } + if (!JS_DefineProperty(aCx, root_obj, + GetNameForProcessID(ProcessID(process)), + processObject, JSPROP_ENUMERATE)) { + return NS_ERROR_FAILURE; + } + + for (const HistogramSnapshotInfo& hData : processHistArray[process]) { + HistogramID id = hData.histogramID; + + JS::Rooted<JSObject*> hobj(aCx, JS_NewPlainObject(aCx)); + if (!hobj) { + return NS_ERROR_FAILURE; + } + + if (NS_FAILED(internal_ReflectHistogramAndSamples( + aCx, hobj, gHistogramInfos[id], hData.data))) { + return NS_ERROR_FAILURE; + } + + if (!JS_DefineProperty(aCx, processObject, gHistogramInfos[id].name(), + hobj, JSPROP_ENUMERATE)) { + return NS_ERROR_FAILURE; + } + } + } + return NS_OK; +} + +nsresult TelemetryHistogram::GetKeyedHistogramSnapshots( + JSContext* aCx, JS::MutableHandleValue aResult, const nsACString& aStore, + unsigned int aDataset, bool aClearSubsession, bool aFilterTest) { + if (!XRE_IsParentProcess()) { + return NS_ERROR_FAILURE; + } + + // Runs without protection from |gTelemetryHistogramMutex| + JS::Rooted<JSObject*> obj(aCx, JS_NewPlainObject(aCx)); + if (!obj) { + return NS_ERROR_FAILURE; + } + aResult.setObject(*obj); + + // Include the GPU process in histogram snapshots only if we actually tried + // to launch a process for it. + bool includeGPUProcess = internal_AttemptedGPUProcess(); + + // Get a snapshot of all the data while holding the mutex. + KeyedHistogramProcessSnapshotsArray processHistArray; + { + StaticMutexAutoLock locker(gTelemetryHistogramMutex); + nsresult rv = internal_GetKeyedHistogramsSnapshot( + locker, aStore, aDataset, aClearSubsession, includeGPUProcess, + aFilterTest, processHistArray); + if (NS_FAILED(rv)) { + return rv; + } + } + + // Mirror the snapshot data to JS, now that we released the mutex. + for (uint32_t process = 0; process < processHistArray.length(); ++process) { + JS::Rooted<JSObject*> processObject(aCx, JS_NewPlainObject(aCx)); + if (!processObject) { + return NS_ERROR_FAILURE; + } + if (!JS_DefineProperty(aCx, obj, GetNameForProcessID(ProcessID(process)), + processObject, JSPROP_ENUMERATE)) { + return NS_ERROR_FAILURE; + } + + for (const KeyedHistogramSnapshotInfo& hData : processHistArray[process]) { + const HistogramInfo& info = gHistogramInfos[hData.histogramId]; + + JS::RootedObject snapshot(aCx, JS_NewPlainObject(aCx)); + if (!snapshot) { + return NS_ERROR_FAILURE; + } + + if (!NS_SUCCEEDED(internal_ReflectKeyedHistogram(hData.data, info, aCx, + snapshot))) { + return NS_ERROR_FAILURE; + } + + if (!JS_DefineProperty(aCx, processObject, info.name(), snapshot, + JSPROP_ENUMERATE)) { + return NS_ERROR_FAILURE; + } + } + } + return NS_OK; +} + +size_t TelemetryHistogram::GetHistogramSizesOfIncludingThis( + mozilla::MallocSizeOf aMallocSizeOf) { + StaticMutexAutoLock locker(gTelemetryHistogramMutex); + + size_t n = 0; + + // If we allocated the array, let's count the number of pointers in there and + // each entry's size. + if (gKeyedHistogramStorage) { + n += HistogramCount * size_t(ProcessID::Count) * sizeof(KeyedHistogram*); + for (size_t i = 0; i < HistogramCount * size_t(ProcessID::Count); ++i) { + if (gKeyedHistogramStorage[i] && + gKeyedHistogramStorage[i] != gExpiredKeyedHistogram) { + n += gKeyedHistogramStorage[i]->SizeOfIncludingThis(aMallocSizeOf); + } + } + } + + // If we allocated the array, let's count the number of pointers in there. + if (gHistogramStorage) { + n += HistogramCount * size_t(ProcessID::Count) * sizeof(Histogram*); + for (size_t i = 0; i < HistogramCount * size_t(ProcessID::Count); ++i) { + if (gHistogramStorage[i] && gHistogramStorage[i] != gExpiredHistogram) { + n += gHistogramStorage[i]->SizeOfIncludingThis(aMallocSizeOf); + } + } + } + + // We only allocate the expired (keyed) histogram once. + if (gExpiredKeyedHistogram) { + n += gExpiredKeyedHistogram->SizeOfIncludingThis(aMallocSizeOf); + } + + if (gExpiredHistogram) { + n += gExpiredHistogram->SizeOfIncludingThis(aMallocSizeOf); + } + + return n; +} + +//////////////////////////////////////////////////////////////////////// +//////////////////////////////////////////////////////////////////////// +// +// PRIVATE: GeckoView specific helpers + +namespace base { +class PersistedSampleSet : public base::Histogram::SampleSet { + public: + explicit PersistedSampleSet(const nsTArray<base::Histogram::Count>& aCounts, + int64_t aSampleSum); +}; + +PersistedSampleSet::PersistedSampleSet( + const nsTArray<base::Histogram::Count>& aCounts, int64_t aSampleSum) { + // Initialize the data in the base class. See Histogram::SampleSet + // for the fields documentation. + const size_t numCounts = aCounts.Length(); + counts_.SetLength(numCounts); + + for (size_t i = 0; i < numCounts; i++) { + counts_[i] = aCounts[i]; + redundant_count_ += aCounts[i]; + } + sum_ = aSampleSum; +}; +} // namespace base + +namespace { +/** + * Helper function to write histogram properties to JSON. + * Please note that this needs to be called between + * StartObjectProperty/EndObject calls that mark the histogram's + * JSON creation. + */ +void internal_ReflectHistogramToJSON(const HistogramSnapshotData& aSnapshot, + mozilla::JSONWriter& aWriter) { + aWriter.IntProperty("sum", aSnapshot.mSampleSum); + + // Fill the "counts" property. + aWriter.StartArrayProperty("counts"); + for (size_t i = 0; i < aSnapshot.mBucketCounts.Length(); i++) { + aWriter.IntElement(aSnapshot.mBucketCounts[i]); + } + aWriter.EndArray(); +} + +bool internal_CanRecordHistogram(const HistogramID id, ProcessID aProcessType) { + // Check if we are allowed to record the data. + if (!CanRecordDataset(gHistogramInfos[id].dataset, internal_CanRecordBase(), + internal_CanRecordExtended())) { + return false; + } + + // Check if we're allowed to record in the given process. + if (aProcessType == ProcessID::Parent && !internal_IsRecordingEnabled(id)) { + return false; + } + + if (aProcessType != ProcessID::Parent && + !CanRecordInProcess(gHistogramInfos[id].record_in_processes, + aProcessType)) { + return false; + } + + // Don't record if the current platform is not enabled + if (!CanRecordProduct(gHistogramInfos[id].products)) { + return false; + } + + return true; +} + +nsresult internal_ParseHistogramData( + JSContext* aCx, JS::HandleId aEntryId, JS::HandleObject aContainerObj, + nsACString& aOutName, nsTArray<base::Histogram::Count>& aOutCountArray, + int64_t& aOutSum) { + // Get the histogram name. + nsAutoJSString histogramName; + if (!histogramName.init(aCx, aEntryId)) { + JS_ClearPendingException(aCx); + return NS_ERROR_FAILURE; + } + + CopyUTF16toUTF8(histogramName, aOutName); + + // Get the data for this histogram. + JS::RootedValue histogramData(aCx); + if (!JS_GetPropertyById(aCx, aContainerObj, aEntryId, &histogramData)) { + JS_ClearPendingException(aCx); + return NS_ERROR_FAILURE; + } + + if (!histogramData.isObject()) { + // base::Histogram data need to be an object. If that's not the case, skip + // it and try to load the rest of the data. + return NS_ERROR_FAILURE; + } + + // Get the "sum" property. + JS::RootedValue sumValue(aCx); + JS::RootedObject histogramObj(aCx, &histogramData.toObject()); + if (!JS_GetProperty(aCx, histogramObj, "sum", &sumValue)) { + JS_ClearPendingException(aCx); + return NS_ERROR_FAILURE; + } + + if (!JS::ToInt64(aCx, sumValue, &aOutSum)) { + JS_ClearPendingException(aCx); + return NS_ERROR_FAILURE; + } + + // Get the "counts" array. + JS::RootedValue countsArray(aCx); + bool countsIsArray = false; + if (!JS_GetProperty(aCx, histogramObj, "counts", &countsArray) || + !JS::IsArrayObject(aCx, countsArray, &countsIsArray)) { + JS_ClearPendingException(aCx); + return NS_ERROR_FAILURE; + } + + if (!countsIsArray) { + // The "counts" property needs to be an array. If this is not the case, + // skip this histogram. + return NS_ERROR_FAILURE; + } + + // Get the length of the array. + uint32_t countsLen = 0; + JS::RootedObject countsArrayObj(aCx, &countsArray.toObject()); + if (!JS::GetArrayLength(aCx, countsArrayObj, &countsLen)) { + JS_ClearPendingException(aCx); + return NS_ERROR_FAILURE; + } + + // Parse the "counts" in the array. + for (uint32_t arrayIdx = 0; arrayIdx < countsLen; arrayIdx++) { + JS::RootedValue elementValue(aCx); + int countAsInt = 0; + if (!JS_GetElement(aCx, countsArrayObj, arrayIdx, &elementValue) || + !JS::ToInt32(aCx, elementValue, &countAsInt)) { + JS_ClearPendingException(aCx); + return NS_ERROR_FAILURE; + } + aOutCountArray.AppendElement(countAsInt); + } + + return NS_OK; +} + +} // Anonymous namespace + +//////////////////////////////////////////////////////////////////////// +//////////////////////////////////////////////////////////////////////// +// +// PUBLIC: GeckoView serialization/deserialization functions. + +nsresult TelemetryHistogram::SerializeHistograms(mozilla::JSONWriter& aWriter) { + MOZ_ASSERT(XRE_IsParentProcess(), + "Only save histograms in the parent process"); + if (!XRE_IsParentProcess()) { + return NS_ERROR_FAILURE; + } + + // Include the GPU process in histogram snapshots only if we actually tried + // to launch a process for it. + bool includeGPUProcess = internal_AttemptedGPUProcess(); + + // Take a snapshot of the histograms. + HistogramProcessSnapshotsArray processHistArray; + { + StaticMutexAutoLock locker(gTelemetryHistogramMutex); + // We always request the "opt-in"/"prerelease" dataset: we internally + // record the right subset, so this will only return "prerelease" if + // it was recorded. + if (NS_FAILED(internal_GetHistogramsSnapshot( + locker, "main"_ns, nsITelemetry::DATASET_PRERELEASE_CHANNELS, + false /* aClearSubsession */, includeGPUProcess, + false /* aFilterTest */, processHistArray))) { + return NS_ERROR_FAILURE; + } + } + + // Make the JSON calls on the stashed histograms for every process + for (uint32_t process = 0; process < processHistArray.length(); ++process) { + aWriter.StartObjectProperty( + mozilla::MakeStringSpan(GetNameForProcessID(ProcessID(process)))); + + for (const HistogramSnapshotInfo& hData : processHistArray[process]) { + HistogramID id = hData.histogramID; + + aWriter.StartObjectProperty( + mozilla::MakeStringSpan(gHistogramInfos[id].name())); + internal_ReflectHistogramToJSON(hData.data, aWriter); + aWriter.EndObject(); + } + aWriter.EndObject(); + } + + return NS_OK; +} + +nsresult TelemetryHistogram::SerializeKeyedHistograms( + mozilla::JSONWriter& aWriter) { + MOZ_ASSERT(XRE_IsParentProcess(), + "Only save keyed histograms in the parent process"); + if (!XRE_IsParentProcess()) { + return NS_ERROR_FAILURE; + } + + // Include the GPU process in histogram snapshots only if we actually tried + // to launch a process for it. + bool includeGPUProcess = internal_AttemptedGPUProcess(); + + // Take a snapshot of the keyed histograms. + KeyedHistogramProcessSnapshotsArray processHistArray; + { + StaticMutexAutoLock locker(gTelemetryHistogramMutex); + // We always request the "opt-in"/"prerelease" dataset: we internally + // record the right subset, so this will only return "prerelease" if + // it was recorded. + if (NS_FAILED(internal_GetKeyedHistogramsSnapshot( + locker, "main"_ns, nsITelemetry::DATASET_PRERELEASE_CHANNELS, + false /* aClearSubsession */, includeGPUProcess, + false /* aFilterTest */, processHistArray))) { + return NS_ERROR_FAILURE; + } + } + + // Serialize the keyed histograms for every process. + for (uint32_t process = 0; process < processHistArray.length(); ++process) { + aWriter.StartObjectProperty( + mozilla::MakeStringSpan(GetNameForProcessID(ProcessID(process)))); + + const KeyedHistogramSnapshotsArray& hArray = processHistArray[process]; + for (size_t i = 0; i < hArray.length(); ++i) { + const KeyedHistogramSnapshotInfo& hData = hArray[i]; + HistogramID id = hData.histogramId; + const HistogramInfo& info = gHistogramInfos[id]; + + aWriter.StartObjectProperty(mozilla::MakeStringSpan(info.name())); + + // Each key is a new object with a "sum" and a "counts" property. + for (auto iter = hData.data.ConstIter(); !iter.Done(); iter.Next()) { + HistogramSnapshotData& keyData = iter.Data(); + aWriter.StartObjectProperty(PromiseFlatCString(iter.Key())); + internal_ReflectHistogramToJSON(keyData, aWriter); + aWriter.EndObject(); + } + + aWriter.EndObject(); + } + aWriter.EndObject(); + } + + return NS_OK; +} + +nsresult TelemetryHistogram::DeserializeHistograms(JSContext* aCx, + JS::HandleValue aData) { + MOZ_ASSERT(XRE_IsParentProcess(), + "Only load histograms in the parent process"); + if (!XRE_IsParentProcess()) { + return NS_ERROR_FAILURE; + } + + // Telemetry is disabled. This should never happen, but let's leave this check + // for consistency with other histogram updates routines. + if (!internal_CanRecordBase()) { + return NS_OK; + } + + typedef mozilla::Tuple<nsCString, nsTArray<base::Histogram::Count>, int64_t> + PersistedHistogramTuple; + typedef mozilla::Vector<PersistedHistogramTuple> PersistedHistogramArray; + typedef mozilla::Vector<PersistedHistogramArray> PersistedHistogramStorage; + + // Before updating the histograms, we need to get the data out of the JS + // wrappers. We can't hold the histogram mutex while handling JS stuff. + // Build a <histogram name, value> map. + JS::RootedObject histogramDataObj(aCx, &aData.toObject()); + JS::Rooted<JS::IdVector> processes(aCx, JS::IdVector(aCx)); + if (!JS_Enumerate(aCx, histogramDataObj, &processes)) { + // We can't even enumerate the processes in the loaded data, so + // there is nothing we could recover from the persistence file. Bail out. + JS_ClearPendingException(aCx); + return NS_ERROR_FAILURE; + } + + // Make sure we have enough storage for all the processes. + PersistedHistogramStorage histogramsToUpdate; + if (!histogramsToUpdate.resize(static_cast<uint32_t>(ProcessID::Count))) { + return NS_ERROR_OUT_OF_MEMORY; + } + + // The following block of code attempts to extract as much data as possible + // from the serialized JSON, even in case of light data corruptions: if, for + // example, the data for a single process is corrupted or is in an unexpected + // form, we press on and attempt to load the data for the other processes. + JS::RootedId process(aCx); + for (auto& processVal : processes) { + // This is required as JS API calls require an Handle<jsid> and not a + // plain jsid. + process = processVal; + // Get the process name. + nsAutoJSString processNameJS; + if (!processNameJS.init(aCx, process)) { + JS_ClearPendingException(aCx); + continue; + } + + // Make sure it's valid. Note that this is safe to call outside + // of a locked section. + NS_ConvertUTF16toUTF8 processName(processNameJS); + ProcessID processID = GetIDForProcessName(processName.get()); + if (processID == ProcessID::Count) { + NS_WARNING( + nsPrintfCString("Failed to get process ID for %s", processName.get()) + .get()); + continue; + } + + // And its probes. + JS::RootedValue processData(aCx); + if (!JS_GetPropertyById(aCx, histogramDataObj, process, &processData)) { + JS_ClearPendingException(aCx); + continue; + } + + if (!processData.isObject()) { + // |processData| should be an object containing histograms. If this is + // not the case, silently skip and try to load the data for the other + // processes. + continue; + } + + // Iterate through each histogram. + JS::RootedObject processDataObj(aCx, &processData.toObject()); + JS::Rooted<JS::IdVector> histograms(aCx, JS::IdVector(aCx)); + if (!JS_Enumerate(aCx, processDataObj, &histograms)) { + JS_ClearPendingException(aCx); + continue; + } + + // Get a reference to the deserialized data for this process. + PersistedHistogramArray& deserializedProcessData = + histogramsToUpdate[static_cast<uint32_t>(processID)]; + + JS::RootedId histogram(aCx); + for (auto& histogramVal : histograms) { + histogram = histogramVal; + + int64_t sum = 0; + nsTArray<base::Histogram::Count> deserializedCounts; + nsCString histogramName; + if (NS_FAILED(internal_ParseHistogramData(aCx, histogram, processDataObj, + histogramName, + deserializedCounts, sum))) { + continue; + } + + // Finally append the deserialized data to the storage. + if (!deserializedProcessData.emplaceBack(MakeTuple( + std::move(histogramName), std::move(deserializedCounts), sum))) { + return NS_ERROR_OUT_OF_MEMORY; + } + } + } + + // Update the histogram storage. + { + StaticMutexAutoLock locker(gTelemetryHistogramMutex); + + for (uint32_t process = 0; process < histogramsToUpdate.length(); + ++process) { + PersistedHistogramArray& processArray = histogramsToUpdate[process]; + + for (auto& histogramData : processArray) { + // Attempt to get the corresponding ID for the deserialized histogram + // name. + HistogramID id; + if (NS_FAILED(internal_GetHistogramIdByName( + locker, mozilla::Get<0>(histogramData), &id))) { + continue; + } + + ProcessID procID = static_cast<ProcessID>(process); + if (!internal_CanRecordHistogram(id, procID)) { + // We're not allowed to record this, so don't try to restore it. + continue; + } + + // Get the Histogram instance: this will instantiate it if it doesn't + // exist. + Histogram* w = internal_GetHistogramById(locker, id, procID); + MOZ_ASSERT(w); + + if (!w || w->IsExpired()) { + continue; + } + + base::Histogram* h = nullptr; + constexpr auto store = "main"_ns; + if (!w->GetHistogram(store, &h)) { + continue; + } + MOZ_ASSERT(h); + + if (!h) { + // Don't restore expired histograms. + continue; + } + + // Make sure that histogram counts have matching sizes. If not, + // |AddSampleSet| will fail and crash. + size_t numCounts = mozilla::Get<1>(histogramData).Length(); + if (h->bucket_count() != numCounts) { + MOZ_ASSERT(false, + "The number of restored buckets does not match with the " + "on in the definition"); + continue; + } + + // Update the data for the histogram. + h->AddSampleSet( + base::PersistedSampleSet(std::move(mozilla::Get<1>(histogramData)), + mozilla::Get<2>(histogramData))); + } + } + } + + return NS_OK; +} + +nsresult TelemetryHistogram::DeserializeKeyedHistograms(JSContext* aCx, + JS::HandleValue aData) { + MOZ_ASSERT(XRE_IsParentProcess(), + "Only load keyed histograms in the parent process"); + if (!XRE_IsParentProcess()) { + return NS_ERROR_FAILURE; + } + + // Telemetry is disabled. This should never happen, but let's leave this check + // for consistency with other histogram updates routines. + if (!internal_CanRecordBase()) { + return NS_OK; + } + + typedef mozilla::Tuple<nsCString, nsCString, nsTArray<base::Histogram::Count>, + int64_t> + PersistedKeyedHistogramTuple; + typedef mozilla::Vector<PersistedKeyedHistogramTuple> + PersistedKeyedHistogramArray; + typedef mozilla::Vector<PersistedKeyedHistogramArray> + PersistedKeyedHistogramStorage; + + // Before updating the histograms, we need to get the data out of the JS + // wrappers. We can't hold the histogram mutex while handling JS stuff. + // Build a <histogram name, value> map. + JS::RootedObject histogramDataObj(aCx, &aData.toObject()); + JS::Rooted<JS::IdVector> processes(aCx, JS::IdVector(aCx)); + if (!JS_Enumerate(aCx, histogramDataObj, &processes)) { + // We can't even enumerate the processes in the loaded data, so + // there is nothing we could recover from the persistence file. Bail out. + JS_ClearPendingException(aCx); + return NS_ERROR_FAILURE; + } + + // Make sure we have enough storage for all the processes. + PersistedKeyedHistogramStorage histogramsToUpdate; + if (!histogramsToUpdate.resize(static_cast<uint32_t>(ProcessID::Count))) { + return NS_ERROR_OUT_OF_MEMORY; + } + + // The following block of code attempts to extract as much data as possible + // from the serialized JSON, even in case of light data corruptions: if, for + // example, the data for a single process is corrupted or is in an unexpected + // form, we press on and attempt to load the data for the other processes. + JS::RootedId process(aCx); + for (auto& processVal : processes) { + // This is required as JS API calls require an Handle<jsid> and not a + // plain jsid. + process = processVal; + // Get the process name. + nsAutoJSString processNameJS; + if (!processNameJS.init(aCx, process)) { + JS_ClearPendingException(aCx); + continue; + } + + // Make sure it's valid. Note that this is safe to call outside + // of a locked section. + NS_ConvertUTF16toUTF8 processName(processNameJS); + ProcessID processID = GetIDForProcessName(processName.get()); + if (processID == ProcessID::Count) { + NS_WARNING( + nsPrintfCString("Failed to get process ID for %s", processName.get()) + .get()); + continue; + } + + // And its probes. + JS::RootedValue processData(aCx); + if (!JS_GetPropertyById(aCx, histogramDataObj, process, &processData)) { + JS_ClearPendingException(aCx); + continue; + } + + if (!processData.isObject()) { + // |processData| should be an object containing histograms. If this is + // not the case, silently skip and try to load the data for the other + // processes. + continue; + } + + // Iterate through each keyed histogram. + JS::RootedObject processDataObj(aCx, &processData.toObject()); + JS::Rooted<JS::IdVector> histograms(aCx, JS::IdVector(aCx)); + if (!JS_Enumerate(aCx, processDataObj, &histograms)) { + JS_ClearPendingException(aCx); + continue; + } + + // Get a reference to the deserialized data for this process. + PersistedKeyedHistogramArray& deserializedProcessData = + histogramsToUpdate[static_cast<uint32_t>(processID)]; + + JS::RootedId histogram(aCx); + for (auto& histogramVal : histograms) { + histogram = histogramVal; + // Get the histogram name. + nsAutoJSString histogramName; + if (!histogramName.init(aCx, histogram)) { + JS_ClearPendingException(aCx); + continue; + } + + // Get the data for this histogram. + JS::RootedValue histogramData(aCx); + if (!JS_GetPropertyById(aCx, processDataObj, histogram, &histogramData)) { + JS_ClearPendingException(aCx); + continue; + } + + // Iterate through each key in the histogram. + JS::RootedObject keysDataObj(aCx, &histogramData.toObject()); + JS::Rooted<JS::IdVector> keys(aCx, JS::IdVector(aCx)); + if (!JS_Enumerate(aCx, keysDataObj, &keys)) { + JS_ClearPendingException(aCx); + continue; + } + + JS::RootedId key(aCx); + for (auto& keyVal : keys) { + key = keyVal; + + int64_t sum = 0; + nsTArray<base::Histogram::Count> deserializedCounts; + nsCString keyName; + if (NS_FAILED(internal_ParseHistogramData( + aCx, key, keysDataObj, keyName, deserializedCounts, sum))) { + continue; + } + + // Finally append the deserialized data to the storage. + if (!deserializedProcessData.emplaceBack(MakeTuple( + nsCString(NS_ConvertUTF16toUTF8(histogramName)), + std::move(keyName), std::move(deserializedCounts), sum))) { + return NS_ERROR_OUT_OF_MEMORY; + } + } + } + } + + // Update the keyed histogram storage. + { + StaticMutexAutoLock locker(gTelemetryHistogramMutex); + + for (uint32_t process = 0; process < histogramsToUpdate.length(); + ++process) { + PersistedKeyedHistogramArray& processArray = histogramsToUpdate[process]; + + for (auto& histogramData : processArray) { + // Attempt to get the corresponding ID for the deserialized histogram + // name. + HistogramID id; + if (NS_FAILED(internal_GetHistogramIdByName( + locker, mozilla::Get<0>(histogramData), &id))) { + continue; + } + + ProcessID procID = static_cast<ProcessID>(process); + if (!internal_CanRecordHistogram(id, procID)) { + // We're not allowed to record this, so don't try to restore it. + continue; + } + + KeyedHistogram* keyed = internal_GetKeyedHistogramById(id, procID); + MOZ_ASSERT(keyed); + + if (!keyed || keyed->IsExpired()) { + // Don't restore if we don't have a destination storage or the + // histogram is expired. + continue; + } + + // Get data for the key we're looking for. + base::Histogram* h = nullptr; + if (NS_FAILED(keyed->GetHistogram( + "main"_ns, mozilla::Get<1>(histogramData), &h))) { + continue; + } + MOZ_ASSERT(h); + + if (!h) { + // Don't restore if we don't have a destination storage. + continue; + } + + // Make sure that histogram counts have matching sizes. If not, + // |AddSampleSet| will fail and crash. + size_t numCounts = mozilla::Get<2>(histogramData).Length(); + if (h->bucket_count() != numCounts) { + MOZ_ASSERT(false, + "The number of restored buckets does not match with the " + "on in the definition"); + continue; + } + + // Update the data for the histogram. + h->AddSampleSet( + base::PersistedSampleSet(std::move(mozilla::Get<2>(histogramData)), + mozilla::Get<3>(histogramData))); + } + } + } + + return NS_OK; +} |