summaryrefslogtreecommitdiffstats
path: root/toolkit/components/telemetry/core/TelemetryScalar.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'toolkit/components/telemetry/core/TelemetryScalar.cpp')
-rw-r--r--toolkit/components/telemetry/core/TelemetryScalar.cpp4202
1 files changed, 4202 insertions, 0 deletions
diff --git a/toolkit/components/telemetry/core/TelemetryScalar.cpp b/toolkit/components/telemetry/core/TelemetryScalar.cpp
new file mode 100644
index 0000000000..0f4bace011
--- /dev/null
+++ b/toolkit/components/telemetry/core/TelemetryScalar.cpp
@@ -0,0 +1,4202 @@
+/* -*- 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 "TelemetryScalar.h"
+
+#include "geckoview/streaming/GeckoViewStreamingTelemetry.h"
+#include "ipc/TelemetryComms.h"
+#include "ipc/TelemetryIPCAccumulator.h"
+#include "js/Array.h" // JS::GetArrayLength, JS::IsArrayObject
+#include "mozilla/dom/ContentParent.h"
+#include "mozilla/dom/PContent.h"
+#include "mozilla/JSONWriter.h"
+#include "mozilla/Preferences.h"
+#include "mozilla/StaticMutex.h"
+#include "mozilla/StaticPtr.h"
+#include "mozilla/Unused.h"
+#include "nsBaseHashtable.h"
+#include "nsClassHashtable.h"
+#include "nsContentUtils.h"
+#include "nsDataHashtable.h"
+#include "nsHashKeys.h"
+#include "nsITelemetry.h"
+#include "nsIVariant.h"
+#include "nsIXPConnect.h"
+#include "nsJSUtils.h"
+#include "nsPrintfCString.h"
+#include "nsThreadUtils.h"
+#include "nsVariant.h"
+#include "TelemetryScalarData.h"
+
+using mozilla::Nothing;
+using mozilla::Preferences;
+using mozilla::Some;
+using mozilla::StaticAutoPtr;
+using mozilla::StaticMutex;
+using mozilla::StaticMutexAutoLock;
+using mozilla::Telemetry::DynamicScalarDefinition;
+using mozilla::Telemetry::KeyedScalarAction;
+using mozilla::Telemetry::ProcessID;
+using mozilla::Telemetry::ScalarAction;
+using mozilla::Telemetry::ScalarActionType;
+using mozilla::Telemetry::ScalarID;
+using mozilla::Telemetry::ScalarVariant;
+using mozilla::Telemetry::Common::AutoHashtable;
+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::IsValidIdentifierString;
+using mozilla::Telemetry::Common::LogToBrowserConsole;
+using mozilla::Telemetry::Common::RecordedProcessType;
+using mozilla::Telemetry::Common::StringHashSet;
+using mozilla::Telemetry::Common::SupportedProduct;
+
+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 (TelemetryScalar::*). If they access shared
+// state, they require the interface function to have acquired
+// |gTelemetryScalarMutex| to ensure thread safety.
+//
+// * Functions named TelemetryScalar::*. This is the external interface.
+// Entries and exits to these functions are serialised using
+// |gTelemetryScalarsMutex|.
+//
+// Avoiding races and deadlocks:
+//
+// All functions in the external interface (TelemetryScalar::*) are
+// serialised using the mutex |gTelemetryScalarsMutex|. This means
+// that the external interface is thread-safe. 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
+//
+// TelemetryScalar::* -> .. any functions .. -> TelemetryScalar::*
+//
+// To reduce the danger of that happening, observe the following rules:
+//
+// * No function in TelemetryScalar::* may directly call, nor take the
+// address of, any other function in TelemetryScalar::*.
+//
+// * No internal function internal_* may call, nor take the address
+// of, any function in TelemetryScalar::*.
+
+////////////////////////////////////////////////////////////////////////
+////////////////////////////////////////////////////////////////////////
+//
+// PRIVATE TYPES
+
+namespace {
+
+const uint32_t kMaximumNumberOfKeys = 100;
+const uint32_t kMaxEventSummaryKeys = 500;
+const uint32_t kMaximumKeyStringLength = 72;
+const uint32_t kMaximumStringValueLength = 50;
+// The category and scalar name maximum lengths are used by the dynamic
+// scalar registration function and must match the constants used by
+// the 'parse_scalars.py' script for static scalars.
+const uint32_t kMaximumCategoryNameLength = 40;
+const uint32_t kMaximumScalarNameLength = 40;
+const uint32_t kScalarCount =
+ static_cast<uint32_t>(mozilla::Telemetry::ScalarID::ScalarCount);
+
+// To stop growing unbounded in memory while waiting for scalar deserialization
+// to finish, we immediately apply pending operations if the array reaches
+// a certain high water mark of elements.
+const size_t kScalarActionsArrayHighWaterMark = 10000;
+
+const char* TEST_SCALAR_PREFIX = "telemetry.test.";
+
+// The max offset supported by gScalarStoresTable for static scalars' stores.
+// Also the sentinel value (with store_count == 0) for just the sole "main"
+// store.
+const uint32_t kMaxStaticStoreOffset = UINT16_MAX;
+
+enum class ScalarResult : uint8_t {
+ // Nothing went wrong.
+ Ok,
+ // General Scalar Errors
+ NotInitialized,
+ CannotUnpackVariant,
+ CannotRecordInProcess,
+ CannotRecordDataset,
+ KeyedTypeMismatch,
+ UnknownScalar,
+ OperationNotSupported,
+ InvalidType,
+ InvalidValue,
+ // Keyed Scalar Errors
+ KeyIsEmpty,
+ KeyTooLong,
+ TooManyKeys,
+ KeyNotAllowed,
+ // String Scalar Errors
+ StringTooLong,
+ // Unsigned Scalar Errors
+ UnsignedNegativeValue,
+ UnsignedTruncatedValue,
+};
+
+// A common identifier for both built-in and dynamic scalars.
+struct ScalarKey {
+ uint32_t id;
+ bool dynamic;
+};
+
+// Dynamic scalar store names.
+StaticAutoPtr<nsTArray<RefPtr<nsAtom>>> gDynamicStoreNames;
+
+/**
+ * Scalar information for dynamic definitions.
+ */
+struct DynamicScalarInfo : BaseScalarInfo {
+ nsCString mDynamicName;
+ bool mDynamicExpiration;
+ uint32_t store_count;
+ uint32_t store_offset;
+
+ DynamicScalarInfo(uint32_t aKind, bool aRecordOnRelease, bool aExpired,
+ const nsACString& aName, bool aKeyed, bool aBuiltin,
+ const nsTArray<nsCString>& aStores)
+ : BaseScalarInfo(aKind,
+ aRecordOnRelease
+ ? nsITelemetry::DATASET_ALL_CHANNELS
+ : nsITelemetry::DATASET_PRERELEASE_CHANNELS,
+ RecordedProcessType::All, aKeyed, 0, 0,
+ GetCurrentProduct(), aBuiltin),
+ mDynamicName(aName),
+ mDynamicExpiration(aExpired) {
+ store_count = aStores.Length();
+ if (store_count == 0) {
+ store_count = 1;
+ store_offset = kMaxStaticStoreOffset;
+ } else {
+ store_offset = kMaxStaticStoreOffset + 1 + gDynamicStoreNames->Length();
+ for (const auto& storeName : aStores) {
+ gDynamicStoreNames->AppendElement(NS_Atomize(storeName));
+ }
+ MOZ_ASSERT(
+ gDynamicStoreNames->Length() < UINT32_MAX - kMaxStaticStoreOffset - 1,
+ "Too many dynamic scalar store names. Overflow.");
+ }
+ };
+
+ // The following functions will read the stored text
+ // instead of looking it up in the statically generated
+ // tables.
+ const char* name() const override;
+ const char* expiration() const override;
+
+ uint32_t storeCount() const override;
+ uint32_t storeOffset() const override;
+};
+
+const char* DynamicScalarInfo::name() const { return mDynamicName.get(); }
+
+const char* DynamicScalarInfo::expiration() const {
+ // Dynamic scalars can either be expired or not (boolean flag).
+ // Return an appropriate version string to leverage the scalar expiration
+ // logic.
+ return mDynamicExpiration ? "1.0" : "never";
+}
+
+uint32_t DynamicScalarInfo::storeOffset() const { return store_offset; }
+uint32_t DynamicScalarInfo::storeCount() const { return store_count; }
+
+typedef nsBaseHashtableET<nsDepCharHashKey, ScalarKey> CharPtrEntryType;
+typedef AutoHashtable<CharPtrEntryType> ScalarMapType;
+
+// Dynamic scalar definitions.
+StaticAutoPtr<nsTArray<DynamicScalarInfo>> gDynamicScalarInfo;
+
+const BaseScalarInfo& internal_GetScalarInfo(const StaticMutexAutoLock& lock,
+ const ScalarKey& aId) {
+ if (!aId.dynamic) {
+ return gScalars[aId.id];
+ }
+
+ return (*gDynamicScalarInfo)[aId.id];
+}
+
+bool IsValidEnumId(mozilla::Telemetry::ScalarID aID) {
+ return aID < mozilla::Telemetry::ScalarID::ScalarCount;
+}
+
+bool internal_IsValidId(const StaticMutexAutoLock& lock, const ScalarKey& aId) {
+ // Please note that this function needs to be called with the scalar
+ // mutex being acquired: other functions might be messing with
+ // |gDynamicScalarInfo|.
+ return aId.dynamic
+ ? (aId.id < gDynamicScalarInfo->Length())
+ : IsValidEnumId(static_cast<mozilla::Telemetry::ScalarID>(aId.id));
+}
+
+/**
+ * Convert a nsIVariant to a mozilla::Variant, which is used for
+ * accumulating child process scalars.
+ */
+ScalarResult GetVariantFromIVariant(nsIVariant* aInput, uint32_t aScalarKind,
+ mozilla::Maybe<ScalarVariant>& aOutput) {
+ switch (aScalarKind) {
+ case nsITelemetry::SCALAR_TYPE_COUNT: {
+ uint32_t val = 0;
+ nsresult rv = aInput->GetAsUint32(&val);
+ if (NS_FAILED(rv)) {
+ return ScalarResult::CannotUnpackVariant;
+ }
+ aOutput = mozilla::Some(mozilla::AsVariant(val));
+ break;
+ }
+ case nsITelemetry::SCALAR_TYPE_STRING: {
+ nsString val;
+ nsresult rv = aInput->GetAsAString(val);
+ if (NS_FAILED(rv)) {
+ return ScalarResult::CannotUnpackVariant;
+ }
+ aOutput = mozilla::Some(mozilla::AsVariant(val));
+ break;
+ }
+ case nsITelemetry::SCALAR_TYPE_BOOLEAN: {
+ bool val = false;
+ nsresult rv = aInput->GetAsBool(&val);
+ if (NS_FAILED(rv)) {
+ return ScalarResult::CannotUnpackVariant;
+ }
+ aOutput = mozilla::Some(mozilla::AsVariant(val));
+ break;
+ }
+ default:
+ MOZ_ASSERT(false, "Unknown scalar kind.");
+ return ScalarResult::UnknownScalar;
+ }
+ return ScalarResult::Ok;
+}
+
+/**
+ * Write a nsIVariant with a JSONWriter, used for GeckoView persistence.
+ */
+nsresult WriteVariantToJSONWriter(
+ uint32_t aScalarType, nsIVariant* aInputValue,
+ const mozilla::Span<const char>& aPropertyName,
+ mozilla::JSONWriter& aWriter) {
+ MOZ_ASSERT(aInputValue);
+
+ switch (aScalarType) {
+ case nsITelemetry::SCALAR_TYPE_COUNT: {
+ uint32_t val = 0;
+ nsresult rv = aInputValue->GetAsUint32(&val);
+ NS_ENSURE_SUCCESS(rv, rv);
+ aWriter.IntProperty(aPropertyName, val);
+ break;
+ }
+ case nsITelemetry::SCALAR_TYPE_STRING: {
+ nsCString val;
+ nsresult rv = aInputValue->GetAsACString(val);
+ NS_ENSURE_SUCCESS(rv, rv);
+ aWriter.StringProperty(aPropertyName, val);
+ break;
+ }
+ case nsITelemetry::SCALAR_TYPE_BOOLEAN: {
+ bool val = false;
+ nsresult rv = aInputValue->GetAsBool(&val);
+ NS_ENSURE_SUCCESS(rv, rv);
+ aWriter.BoolProperty(aPropertyName, val);
+ break;
+ }
+ default:
+ MOZ_ASSERT(false, "Unknown scalar kind.");
+ return NS_ERROR_FAILURE;
+ }
+
+ return NS_OK;
+}
+
+// Implements the methods for ScalarInfo.
+const char* ScalarInfo::name() const {
+ return &gScalarsStringTable[this->name_offset];
+}
+
+const char* ScalarInfo::expiration() const {
+ return &gScalarsStringTable[this->expiration_offset];
+}
+
+/**
+ * The base scalar object, that serves as a common ancestor for storage
+ * purposes.
+ */
+class ScalarBase {
+ public:
+ explicit ScalarBase(const BaseScalarInfo& aInfo)
+ : mStoreCount(aInfo.storeCount()),
+ mStoreOffset(aInfo.storeOffset()),
+ mStoreHasValue(mStoreCount),
+ mName(aInfo.name()) {
+ mStoreHasValue.SetLength(mStoreCount);
+ for (auto& val : mStoreHasValue) {
+ val = false;
+ }
+ };
+ virtual ~ScalarBase() = default;
+
+ // Set, Add and SetMaximum functions as described in the Telemetry IDL.
+ virtual ScalarResult SetValue(nsIVariant* aValue) = 0;
+ virtual ScalarResult AddValue(nsIVariant* aValue) {
+ return ScalarResult::OperationNotSupported;
+ }
+ virtual ScalarResult SetMaximum(nsIVariant* aValue) {
+ return ScalarResult::OperationNotSupported;
+ }
+
+ // Convenience methods used by the C++ API.
+ virtual void SetValue(uint32_t aValue) {
+ mozilla::Unused << HandleUnsupported();
+ }
+ virtual ScalarResult SetValue(const nsAString& aValue) {
+ return HandleUnsupported();
+ }
+ virtual void SetValue(bool aValue) { mozilla::Unused << HandleUnsupported(); }
+ virtual void AddValue(uint32_t aValue) {
+ mozilla::Unused << HandleUnsupported();
+ }
+ virtual void SetMaximum(uint32_t aValue) {
+ mozilla::Unused << HandleUnsupported();
+ }
+
+ // GetValue is used to get the value of the scalar when persisting it to JS.
+ virtual nsresult GetValue(const nsACString& aStoreName, bool aClearStore,
+ nsCOMPtr<nsIVariant>& aResult) = 0;
+
+ // To measure the memory stats.
+ size_t SizeOfExcludingThis(mozilla::MallocSizeOf aMallocSizeOf) const;
+ virtual size_t SizeOfIncludingThis(
+ mozilla::MallocSizeOf aMallocSizeOf) const = 0;
+
+ protected:
+ bool HasValueInStore(size_t aStoreIndex) const;
+ void ClearValueInStore(size_t aStoreIndex);
+ void SetValueInStores();
+ nsresult StoreIndex(const nsACString& aStoreName, size_t* aStoreIndex) const;
+
+ private:
+ ScalarResult HandleUnsupported() const;
+
+ const uint32_t mStoreCount;
+ const uint32_t mStoreOffset;
+ nsTArray<bool> mStoreHasValue;
+
+ protected:
+ const nsCString mName;
+};
+
+ScalarResult ScalarBase::HandleUnsupported() const {
+ MOZ_ASSERT(false, "This operation is not support for this scalar type.");
+ return ScalarResult::OperationNotSupported;
+}
+
+bool ScalarBase::HasValueInStore(size_t aStoreIndex) const {
+ MOZ_ASSERT(aStoreIndex < mStoreHasValue.Length(),
+ "Invalid scalar store index.");
+ return mStoreHasValue[aStoreIndex];
+}
+
+void ScalarBase::ClearValueInStore(size_t aStoreIndex) {
+ MOZ_ASSERT(aStoreIndex < mStoreHasValue.Length(),
+ "Invalid scalar store index to clear.");
+ mStoreHasValue[aStoreIndex] = false;
+}
+
+void ScalarBase::SetValueInStores() {
+ for (auto& val : mStoreHasValue) {
+ val = true;
+ }
+}
+
+nsresult ScalarBase::StoreIndex(const nsACString& aStoreName,
+ size_t* aStoreIndex) const {
+ if (mStoreCount == 1 && mStoreOffset == kMaxStaticStoreOffset) {
+ // This Scalar is only in the "main" store.
+ if (aStoreName.EqualsLiteral("main")) {
+ *aStoreIndex = 0;
+ return NS_OK;
+ }
+ return NS_ERROR_NO_CONTENT;
+ }
+
+ // Multiple stores. Linear scan to find one that matches aStoreName.
+ // Dynamic Scalars start at kMaxStaticStoreOffset + 1
+ if (mStoreOffset > kMaxStaticStoreOffset) {
+ auto dynamicOffset = mStoreOffset - kMaxStaticStoreOffset - 1;
+ for (uint32_t i = 0; i < mStoreCount; ++i) {
+ auto scalarStore = (*gDynamicStoreNames)[dynamicOffset + i];
+ if (nsAtomCString(scalarStore).Equals(aStoreName)) {
+ *aStoreIndex = i;
+ return NS_OK;
+ }
+ }
+ return NS_ERROR_NO_CONTENT;
+ }
+
+ // Static Scalars are similar.
+ for (uint32_t i = 0; i < mStoreCount; ++i) {
+ uint32_t stringIndex = gScalarStoresTable[mStoreOffset + i];
+ if (aStoreName.EqualsASCII(&gScalarsStringTable[stringIndex])) {
+ *aStoreIndex = i;
+ return NS_OK;
+ }
+ }
+ return NS_ERROR_NO_CONTENT;
+}
+
+size_t ScalarBase::SizeOfExcludingThis(
+ mozilla::MallocSizeOf aMallocSizeOf) const {
+ return mStoreHasValue.ShallowSizeOfExcludingThis(aMallocSizeOf);
+}
+
+/**
+ * The implementation for the unsigned int scalar type.
+ */
+class ScalarUnsigned : public ScalarBase {
+ public:
+ using ScalarBase::SetValue;
+
+ explicit ScalarUnsigned(const BaseScalarInfo& aInfo)
+ : ScalarBase(aInfo), mStorage(aInfo.storeCount()) {
+ mStorage.SetLength(aInfo.storeCount());
+ for (auto& val : mStorage) {
+ val = 0;
+ }
+ };
+
+ ~ScalarUnsigned() override = default;
+
+ ScalarResult SetValue(nsIVariant* aValue) final;
+ void SetValue(uint32_t aValue) final;
+ ScalarResult AddValue(nsIVariant* aValue) final;
+ void AddValue(uint32_t aValue) final;
+ ScalarResult SetMaximum(nsIVariant* aValue) final;
+ void SetMaximum(uint32_t aValue) final;
+ nsresult GetValue(const nsACString& aStoreName, bool aClearStore,
+ nsCOMPtr<nsIVariant>& aResult) final;
+ size_t SizeOfIncludingThis(mozilla::MallocSizeOf aMallocSizeOf) const final;
+
+ private:
+ nsTArray<uint32_t> mStorage;
+
+ ScalarResult CheckInput(nsIVariant* aValue);
+
+ // Prevent copying.
+ ScalarUnsigned(const ScalarUnsigned& aOther) = delete;
+ void operator=(const ScalarUnsigned& aOther) = delete;
+};
+
+ScalarResult ScalarUnsigned::SetValue(nsIVariant* aValue) {
+ ScalarResult sr = CheckInput(aValue);
+ if (sr == ScalarResult::UnsignedNegativeValue) {
+ return sr;
+ }
+
+ uint32_t value = 0;
+ if (NS_FAILED(aValue->GetAsUint32(&value))) {
+ return ScalarResult::InvalidValue;
+ }
+
+ SetValue(value);
+ return sr;
+}
+
+void ScalarUnsigned::SetValue(uint32_t aValue) {
+ if (GetCurrentProduct() == SupportedProduct::GeckoviewStreaming) {
+ GeckoViewStreamingTelemetry::UintScalarSet(mName, aValue);
+ return;
+ }
+ for (auto& val : mStorage) {
+ val = aValue;
+ }
+ SetValueInStores();
+}
+
+ScalarResult ScalarUnsigned::AddValue(nsIVariant* aValue) {
+ ScalarResult sr = CheckInput(aValue);
+ if (sr == ScalarResult::UnsignedNegativeValue) {
+ return sr;
+ }
+
+ uint32_t newAddend = 0;
+ nsresult rv = aValue->GetAsUint32(&newAddend);
+ if (NS_FAILED(rv)) {
+ return ScalarResult::InvalidValue;
+ }
+
+ AddValue(newAddend);
+ return sr;
+}
+
+void ScalarUnsigned::AddValue(uint32_t aValue) {
+ MOZ_ASSERT(GetCurrentProduct() != SupportedProduct::GeckoviewStreaming);
+ for (auto& val : mStorage) {
+ val += aValue;
+ }
+ SetValueInStores();
+}
+
+ScalarResult ScalarUnsigned::SetMaximum(nsIVariant* aValue) {
+ MOZ_ASSERT(GetCurrentProduct() != SupportedProduct::GeckoviewStreaming);
+ ScalarResult sr = CheckInput(aValue);
+ if (sr == ScalarResult::UnsignedNegativeValue) {
+ return sr;
+ }
+
+ uint32_t newValue = 0;
+ nsresult rv = aValue->GetAsUint32(&newValue);
+ if (NS_FAILED(rv)) {
+ return ScalarResult::InvalidValue;
+ }
+
+ SetMaximum(newValue);
+ return sr;
+}
+
+void ScalarUnsigned::SetMaximum(uint32_t aValue) {
+ for (auto& val : mStorage) {
+ if (aValue > val) {
+ val = aValue;
+ }
+ }
+ SetValueInStores();
+}
+
+nsresult ScalarUnsigned::GetValue(const nsACString& aStoreName,
+ bool aClearStore,
+ nsCOMPtr<nsIVariant>& aResult) {
+ size_t storeIndex = 0;
+ nsresult rv = StoreIndex(aStoreName, &storeIndex);
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+ if (!HasValueInStore(storeIndex)) {
+ return NS_ERROR_NO_CONTENT;
+ }
+ nsCOMPtr<nsIWritableVariant> outVar(new nsVariant());
+ rv = outVar->SetAsUint32(mStorage[storeIndex]);
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+ aResult = std::move(outVar);
+ if (aClearStore) {
+ mStorage[storeIndex] = 0;
+ ClearValueInStore(storeIndex);
+ }
+ return NS_OK;
+}
+
+size_t ScalarUnsigned::SizeOfIncludingThis(
+ mozilla::MallocSizeOf aMallocSizeOf) const {
+ size_t n = aMallocSizeOf(this);
+ n += ScalarBase::SizeOfExcludingThis(aMallocSizeOf);
+ n += mStorage.ShallowSizeOfExcludingThis(aMallocSizeOf);
+ return n;
+}
+
+ScalarResult ScalarUnsigned::CheckInput(nsIVariant* aValue) {
+ // If this is a floating point value/double, we will probably get truncated.
+ uint16_t type = aValue->GetDataType();
+ if (type == nsIDataType::VTYPE_FLOAT || type == nsIDataType::VTYPE_DOUBLE) {
+ return ScalarResult::UnsignedTruncatedValue;
+ }
+
+ int32_t signedTest;
+ // If we're able to cast the number to an int, check its sign.
+ // Warn the user if he's trying to set the unsigned scalar to a negative
+ // number.
+ if (NS_SUCCEEDED(aValue->GetAsInt32(&signedTest)) && signedTest < 0) {
+ return ScalarResult::UnsignedNegativeValue;
+ }
+ return ScalarResult::Ok;
+}
+
+/**
+ * The implementation for the string scalar type.
+ */
+class ScalarString : public ScalarBase {
+ public:
+ using ScalarBase::SetValue;
+
+ explicit ScalarString(const BaseScalarInfo& aInfo)
+ : ScalarBase(aInfo), mStorage(aInfo.storeCount()) {
+ mStorage.SetLength(aInfo.storeCount());
+ };
+
+ ~ScalarString() override = default;
+
+ ScalarResult SetValue(nsIVariant* aValue) final;
+ ScalarResult SetValue(const nsAString& aValue) final;
+ nsresult GetValue(const nsACString& aStoreName, bool aClearStore,
+ nsCOMPtr<nsIVariant>& aResult) final;
+ size_t SizeOfIncludingThis(mozilla::MallocSizeOf aMallocSizeOf) const final;
+
+ private:
+ nsTArray<nsString> mStorage;
+
+ // Prevent copying.
+ ScalarString(const ScalarString& aOther) = delete;
+ void operator=(const ScalarString& aOther) = delete;
+};
+
+ScalarResult ScalarString::SetValue(nsIVariant* aValue) {
+ // Check that we got the correct data type.
+ uint16_t type = aValue->GetDataType();
+ if (type != nsIDataType::VTYPE_CHAR && type != nsIDataType::VTYPE_WCHAR &&
+ type != nsIDataType::VTYPE_CHAR_STR &&
+ type != nsIDataType::VTYPE_WCHAR_STR &&
+ type != nsIDataType::VTYPE_STRING_SIZE_IS &&
+ type != nsIDataType::VTYPE_WSTRING_SIZE_IS &&
+ type != nsIDataType::VTYPE_UTF8STRING &&
+ type != nsIDataType::VTYPE_CSTRING &&
+ type != nsIDataType::VTYPE_ASTRING) {
+ return ScalarResult::InvalidType;
+ }
+
+ nsAutoString convertedString;
+ nsresult rv = aValue->GetAsAString(convertedString);
+ if (NS_FAILED(rv)) {
+ return ScalarResult::InvalidValue;
+ }
+ return SetValue(convertedString);
+};
+
+ScalarResult ScalarString::SetValue(const nsAString& aValue) {
+ auto str = Substring(aValue, 0, kMaximumStringValueLength);
+ if (GetCurrentProduct() == SupportedProduct::GeckoviewStreaming) {
+ GeckoViewStreamingTelemetry::StringScalarSet(mName,
+ NS_ConvertUTF16toUTF8(str));
+ return aValue.Length() > kMaximumStringValueLength
+ ? ScalarResult::StringTooLong
+ : ScalarResult::Ok;
+ }
+ for (auto& val : mStorage) {
+ val.Assign(str);
+ }
+ SetValueInStores();
+ if (aValue.Length() > kMaximumStringValueLength) {
+ return ScalarResult::StringTooLong;
+ }
+ return ScalarResult::Ok;
+}
+
+nsresult ScalarString::GetValue(const nsACString& aStoreName, bool aClearStore,
+ nsCOMPtr<nsIVariant>& aResult) {
+ nsCOMPtr<nsIWritableVariant> outVar(new nsVariant());
+ size_t storeIndex = 0;
+ nsresult rv = StoreIndex(aStoreName, &storeIndex);
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+ if (!HasValueInStore(storeIndex)) {
+ return NS_ERROR_NO_CONTENT;
+ }
+ rv = outVar->SetAsAString(mStorage[storeIndex]);
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+ if (aClearStore) {
+ ClearValueInStore(storeIndex);
+ }
+ aResult = std::move(outVar);
+ return NS_OK;
+}
+
+size_t ScalarString::SizeOfIncludingThis(
+ mozilla::MallocSizeOf aMallocSizeOf) const {
+ size_t n = aMallocSizeOf(this);
+ n += ScalarBase::SizeOfExcludingThis(aMallocSizeOf);
+ n += mStorage.ShallowSizeOfExcludingThis(aMallocSizeOf);
+ for (auto& val : mStorage) {
+ n += val.SizeOfExcludingThisIfUnshared(aMallocSizeOf);
+ }
+ return n;
+}
+
+/**
+ * The implementation for the boolean scalar type.
+ */
+class ScalarBoolean : public ScalarBase {
+ public:
+ using ScalarBase::SetValue;
+
+ explicit ScalarBoolean(const BaseScalarInfo& aInfo)
+ : ScalarBase(aInfo), mStorage(aInfo.storeCount()) {
+ mStorage.SetLength(aInfo.storeCount());
+ for (auto& val : mStorage) {
+ val = false;
+ }
+ };
+
+ ~ScalarBoolean() override = default;
+
+ ScalarResult SetValue(nsIVariant* aValue) final;
+ void SetValue(bool aValue) final;
+ nsresult GetValue(const nsACString& aStoreName, bool aClearStore,
+ nsCOMPtr<nsIVariant>& aResult) final;
+ size_t SizeOfIncludingThis(mozilla::MallocSizeOf aMallocSizeOf) const final;
+
+ private:
+ nsTArray<bool> mStorage;
+
+ // Prevent copying.
+ ScalarBoolean(const ScalarBoolean& aOther) = delete;
+ void operator=(const ScalarBoolean& aOther) = delete;
+};
+
+ScalarResult ScalarBoolean::SetValue(nsIVariant* aValue) {
+ // Check that we got the correct data type.
+ uint16_t type = aValue->GetDataType();
+ if (type != nsIDataType::VTYPE_BOOL && type != nsIDataType::VTYPE_INT8 &&
+ type != nsIDataType::VTYPE_INT16 && type != nsIDataType::VTYPE_INT32 &&
+ type != nsIDataType::VTYPE_INT64 && type != nsIDataType::VTYPE_UINT8 &&
+ type != nsIDataType::VTYPE_UINT16 && type != nsIDataType::VTYPE_UINT32 &&
+ type != nsIDataType::VTYPE_UINT64) {
+ return ScalarResult::InvalidType;
+ }
+
+ bool value = false;
+ if (NS_FAILED(aValue->GetAsBool(&value))) {
+ return ScalarResult::InvalidValue;
+ }
+ SetValue(value);
+ return ScalarResult::Ok;
+};
+
+void ScalarBoolean::SetValue(bool aValue) {
+ if (GetCurrentProduct() == SupportedProduct::GeckoviewStreaming) {
+ GeckoViewStreamingTelemetry::BoolScalarSet(mName, aValue);
+ return;
+ }
+ for (auto& val : mStorage) {
+ val = aValue;
+ }
+ SetValueInStores();
+}
+
+nsresult ScalarBoolean::GetValue(const nsACString& aStoreName, bool aClearStore,
+ nsCOMPtr<nsIVariant>& aResult) {
+ nsCOMPtr<nsIWritableVariant> outVar(new nsVariant());
+ size_t storeIndex = 0;
+ nsresult rv = StoreIndex(aStoreName, &storeIndex);
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+ if (!HasValueInStore(storeIndex)) {
+ return NS_ERROR_NO_CONTENT;
+ }
+ if (aClearStore) {
+ ClearValueInStore(storeIndex);
+ }
+ rv = outVar->SetAsBool(mStorage[storeIndex]);
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+ aResult = std::move(outVar);
+ return NS_OK;
+}
+
+size_t ScalarBoolean::SizeOfIncludingThis(
+ mozilla::MallocSizeOf aMallocSizeOf) const {
+ size_t n = aMallocSizeOf(this);
+ n += ScalarBase::SizeOfExcludingThis(aMallocSizeOf);
+ n += mStorage.ShallowSizeOfExcludingThis(aMallocSizeOf);
+ return n;
+}
+
+/**
+ * Allocate a scalar class given the scalar info.
+ *
+ * @param aInfo The informations for the scalar coming from the definition file.
+ * @return nullptr if the scalar type is unknown, otherwise a valid pointer to
+ * the scalar type.
+ */
+ScalarBase* internal_ScalarAllocate(const BaseScalarInfo& aInfo) {
+ ScalarBase* scalar = nullptr;
+ switch (aInfo.kind) {
+ case nsITelemetry::SCALAR_TYPE_COUNT:
+ scalar = new ScalarUnsigned(aInfo);
+ break;
+ case nsITelemetry::SCALAR_TYPE_STRING:
+ scalar = new ScalarString(aInfo);
+ break;
+ case nsITelemetry::SCALAR_TYPE_BOOLEAN:
+ scalar = new ScalarBoolean(aInfo);
+ break;
+ default:
+ MOZ_ASSERT(false, "Invalid scalar type");
+ }
+ return scalar;
+}
+
+/**
+ * The implementation for the keyed scalar type.
+ */
+class KeyedScalar {
+ public:
+ typedef std::pair<nsCString, nsCOMPtr<nsIVariant>> KeyValuePair;
+
+ // We store the name instead of a reference to the BaseScalarInfo because
+ // the BaseScalarInfo can move if it's from a dynamic scalar.
+ explicit KeyedScalar(const BaseScalarInfo& info)
+ : mScalarName(info.name()),
+ mScalarKeyCount(info.key_count),
+ mScalarKeyOffset(info.key_offset),
+ mMaximumNumberOfKeys(kMaximumNumberOfKeys){};
+ ~KeyedScalar() = default;
+
+ // Set, Add and SetMaximum functions as described in the Telemetry IDL.
+ // These methods implicitly instantiate a Scalar[*] for each key.
+ ScalarResult SetValue(const StaticMutexAutoLock& locker,
+ const nsAString& aKey, nsIVariant* aValue);
+ ScalarResult AddValue(const StaticMutexAutoLock& locker,
+ const nsAString& aKey, nsIVariant* aValue);
+ ScalarResult SetMaximum(const StaticMutexAutoLock& locker,
+ const nsAString& aKey, nsIVariant* aValue);
+
+ // Convenience methods used by the C++ API.
+ void SetValue(const StaticMutexAutoLock& locker, const nsAString& aKey,
+ uint32_t aValue);
+ void SetValue(const StaticMutexAutoLock& locker, const nsAString& aKey,
+ bool aValue);
+ void AddValue(const StaticMutexAutoLock& locker, const nsAString& aKey,
+ uint32_t aValue);
+ void SetMaximum(const StaticMutexAutoLock& locker, const nsAString& aKey,
+ uint32_t aValue);
+
+ // GetValue is used to get the key-value pairs stored in the keyed scalar
+ // when persisting it to JS.
+ nsresult GetValue(const nsACString& aStoreName, bool aClearStorage,
+ nsTArray<KeyValuePair>& aValues);
+
+ // To measure the memory stats.
+ size_t SizeOfIncludingThis(mozilla::MallocSizeOf aMallocSizeOf);
+
+ // To permit more keys than normal.
+ void SetMaximumNumberOfKeys(uint32_t aMaximumNumberOfKeys) {
+ mMaximumNumberOfKeys = aMaximumNumberOfKeys;
+ };
+
+ private:
+ typedef nsClassHashtable<nsCStringHashKey, ScalarBase> ScalarKeysMapType;
+
+ const nsCString mScalarName;
+ ScalarKeysMapType mScalarKeys;
+ uint32_t mScalarKeyCount;
+ uint32_t mScalarKeyOffset;
+ uint32_t mMaximumNumberOfKeys;
+
+ ScalarResult GetScalarForKey(const StaticMutexAutoLock& locker,
+ const nsAString& aKey, ScalarBase** aRet);
+
+ bool AllowsKey(const nsAString& aKey) const;
+};
+
+ScalarResult KeyedScalar::SetValue(const StaticMutexAutoLock& locker,
+ const nsAString& aKey, nsIVariant* aValue) {
+ ScalarBase* scalar = nullptr;
+ ScalarResult sr = GetScalarForKey(locker, aKey, &scalar);
+ if (sr != ScalarResult::Ok) {
+ return sr;
+ }
+
+ return scalar->SetValue(aValue);
+}
+
+ScalarResult KeyedScalar::AddValue(const StaticMutexAutoLock& locker,
+ const nsAString& aKey, nsIVariant* aValue) {
+ ScalarBase* scalar = nullptr;
+ ScalarResult sr = GetScalarForKey(locker, aKey, &scalar);
+ if (sr != ScalarResult::Ok) {
+ return sr;
+ }
+
+ return scalar->AddValue(aValue);
+}
+
+ScalarResult KeyedScalar::SetMaximum(const StaticMutexAutoLock& locker,
+ const nsAString& aKey,
+ nsIVariant* aValue) {
+ ScalarBase* scalar = nullptr;
+ ScalarResult sr = GetScalarForKey(locker, aKey, &scalar);
+ if (sr != ScalarResult::Ok) {
+ return sr;
+ }
+
+ return scalar->SetMaximum(aValue);
+}
+
+void KeyedScalar::SetValue(const StaticMutexAutoLock& locker,
+ const nsAString& aKey, uint32_t aValue) {
+ ScalarBase* scalar = nullptr;
+ ScalarResult sr = GetScalarForKey(locker, aKey, &scalar);
+
+ if (sr != ScalarResult::Ok) {
+ // Bug 1451813 - We now report which scalars exceed the key limit in
+ // telemetry.keyed_scalars_exceed_limit.
+ if (sr == ScalarResult::KeyTooLong) {
+ MOZ_ASSERT(false, "Key too long to be recorded in the scalar.");
+ }
+ return;
+ }
+
+ return scalar->SetValue(aValue);
+}
+
+void KeyedScalar::SetValue(const StaticMutexAutoLock& locker,
+ const nsAString& aKey, bool aValue) {
+ ScalarBase* scalar = nullptr;
+ ScalarResult sr = GetScalarForKey(locker, aKey, &scalar);
+
+ if (sr != ScalarResult::Ok) {
+ // Bug 1451813 - We now report which scalars exceed the key limit in
+ // telemetry.keyed_scalars_exceed_limit.
+ if (sr == ScalarResult::KeyTooLong) {
+ MOZ_ASSERT(false, "Key too long to be recorded in the scalar.");
+ }
+ return;
+ }
+
+ return scalar->SetValue(aValue);
+}
+
+void KeyedScalar::AddValue(const StaticMutexAutoLock& locker,
+ const nsAString& aKey, uint32_t aValue) {
+ ScalarBase* scalar = nullptr;
+ ScalarResult sr = GetScalarForKey(locker, aKey, &scalar);
+
+ if (sr != ScalarResult::Ok) {
+ // Bug 1451813 - We now report which scalars exceed the key limit in
+ // telemetry.keyed_scalars_exceed_limit.
+ if (sr == ScalarResult::KeyTooLong) {
+ MOZ_ASSERT(false, "Key too long to be recorded in the scalar.");
+ }
+ return;
+ }
+
+ return scalar->AddValue(aValue);
+}
+
+void KeyedScalar::SetMaximum(const StaticMutexAutoLock& locker,
+ const nsAString& aKey, uint32_t aValue) {
+ ScalarBase* scalar = nullptr;
+ ScalarResult sr = GetScalarForKey(locker, aKey, &scalar);
+
+ if (sr != ScalarResult::Ok) {
+ // Bug 1451813 - We now report which scalars exceed the key limit in
+ // telemetry.keyed_scalars_exceed_limit.
+ if (sr == ScalarResult::KeyTooLong) {
+ MOZ_ASSERT(false, "Key too long to be recorded in the scalar.");
+ }
+
+ return;
+ }
+
+ return scalar->SetMaximum(aValue);
+}
+
+/**
+ * Get a key-value array with the values for the Keyed Scalar.
+ * @param aValue The array that will hold the key-value pairs.
+ * @return {nsresult} NS_OK or an error value as reported by the
+ * the specific scalar objects implementations (e.g.
+ * ScalarUnsigned).
+ */
+nsresult KeyedScalar::GetValue(const nsACString& aStoreName, bool aClearStorage,
+ nsTArray<KeyValuePair>& aValues) {
+ for (auto iter = mScalarKeys.ConstIter(); !iter.Done(); iter.Next()) {
+ ScalarBase* scalar = iter.UserData();
+
+ // Get the scalar value.
+ nsCOMPtr<nsIVariant> scalarValue;
+ nsresult rv = scalar->GetValue(aStoreName, aClearStorage, scalarValue);
+ if (rv == NS_ERROR_NO_CONTENT) {
+ // No value for this store.
+ continue;
+ }
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+
+ // Append it to value list.
+ aValues.AppendElement(std::make_pair(nsCString(iter.Key()), scalarValue));
+ }
+
+ return NS_OK;
+}
+
+// Forward declaration
+nsresult internal_GetKeyedScalarByEnum(const StaticMutexAutoLock& lock,
+ const ScalarKey& aId,
+ ProcessID aProcessStorage,
+ KeyedScalar** aRet);
+
+// Forward declaration
+nsresult internal_GetEnumByScalarName(const StaticMutexAutoLock& lock,
+ const nsACString& aName, ScalarKey* aId);
+
+/**
+ * Get the scalar for the referenced key.
+ * If there's no such key, instantiate a new Scalar object with the
+ * same type of the Keyed scalar and create the key.
+ */
+ScalarResult KeyedScalar::GetScalarForKey(const StaticMutexAutoLock& locker,
+ const nsAString& aKey,
+ ScalarBase** aRet) {
+ if (aKey.IsEmpty()) {
+ return ScalarResult::KeyIsEmpty;
+ }
+
+ if (!AllowsKey(aKey)) {
+ KeyedScalar* scalarUnknown = nullptr;
+ ScalarKey scalarUnknownUniqueId{
+ static_cast<uint32_t>(
+ mozilla::Telemetry::ScalarID::TELEMETRY_KEYED_SCALARS_UNKNOWN_KEYS),
+ false};
+ ProcessID process = ProcessID::Parent;
+ nsresult rv = internal_GetKeyedScalarByEnum(locker, scalarUnknownUniqueId,
+ process, &scalarUnknown);
+ if (NS_FAILED(rv)) {
+ return ScalarResult::TooManyKeys;
+ }
+ scalarUnknown->AddValue(locker, NS_ConvertUTF8toUTF16(mScalarName), 1);
+
+ return ScalarResult::KeyNotAllowed;
+ }
+
+ if (aKey.Length() > kMaximumKeyStringLength) {
+ return ScalarResult::KeyTooLong;
+ }
+
+ NS_ConvertUTF16toUTF8 utf8Key(aKey);
+
+ ScalarBase* scalar = nullptr;
+ if (mScalarKeys.Get(utf8Key, &scalar)) {
+ *aRet = scalar;
+ return ScalarResult::Ok;
+ }
+
+ ScalarKey uniqueId;
+ nsresult rv = internal_GetEnumByScalarName(locker, mScalarName, &uniqueId);
+ if (NS_FAILED(rv)) {
+ return (rv == NS_ERROR_FAILURE) ? ScalarResult::NotInitialized
+ : ScalarResult::UnknownScalar;
+ }
+
+ const BaseScalarInfo& info = internal_GetScalarInfo(locker, uniqueId);
+ if (mScalarKeys.Count() >= mMaximumNumberOfKeys) {
+ if (aKey.EqualsLiteral("telemetry.keyed_scalars_exceed_limit")) {
+ return ScalarResult::TooManyKeys;
+ }
+
+ KeyedScalar* scalarExceed = nullptr;
+
+ ScalarKey uniqueId{
+ static_cast<uint32_t>(
+ mozilla::Telemetry::ScalarID::TELEMETRY_KEYED_SCALARS_EXCEED_LIMIT),
+ false};
+
+ ProcessID process = ProcessID::Parent;
+ nsresult rv =
+ internal_GetKeyedScalarByEnum(locker, uniqueId, process, &scalarExceed);
+
+ if (NS_FAILED(rv)) {
+ return ScalarResult::TooManyKeys;
+ }
+
+ scalarExceed->AddValue(locker, NS_ConvertUTF8toUTF16(info.name()), 1);
+
+ return ScalarResult::TooManyKeys;
+ }
+
+ scalar = internal_ScalarAllocate(info);
+ if (!scalar) {
+ return ScalarResult::InvalidType;
+ }
+
+ mScalarKeys.Put(utf8Key, scalar);
+
+ *aRet = scalar;
+ return ScalarResult::Ok;
+}
+
+size_t KeyedScalar::SizeOfIncludingThis(mozilla::MallocSizeOf aMallocSizeOf) {
+ size_t n = aMallocSizeOf(this);
+ for (auto iter = mScalarKeys.Iter(); !iter.Done(); iter.Next()) {
+ ScalarBase* scalar = iter.UserData();
+ n += scalar->SizeOfIncludingThis(aMallocSizeOf);
+ }
+ return n;
+}
+
+bool KeyedScalar::AllowsKey(const nsAString& aKey) const {
+ // If we didn't specify a list of allowed keys, just return true.
+ if (mScalarKeyCount == 0) {
+ return true;
+ }
+
+ for (uint32_t i = 0; i < mScalarKeyCount; ++i) {
+ uint32_t stringIndex = gScalarKeysTable[mScalarKeyOffset + i];
+ if (aKey.EqualsASCII(&gScalarsStringTable[stringIndex])) {
+ return true;
+ }
+ }
+
+ return false;
+}
+
+typedef nsUint32HashKey ScalarIDHashKey;
+typedef nsUint32HashKey ProcessIDHashKey;
+typedef nsClassHashtable<ScalarIDHashKey, ScalarBase> ScalarStorageMapType;
+typedef nsClassHashtable<ScalarIDHashKey, KeyedScalar>
+ KeyedScalarStorageMapType;
+typedef nsClassHashtable<ProcessIDHashKey, ScalarStorageMapType>
+ ProcessesScalarsMapType;
+typedef nsClassHashtable<ProcessIDHashKey, KeyedScalarStorageMapType>
+ ProcessesKeyedScalarsMapType;
+
+typedef mozilla::Tuple<const char*, nsCOMPtr<nsIVariant>, uint32_t>
+ ScalarDataTuple;
+typedef nsTArray<ScalarDataTuple> ScalarTupleArray;
+typedef nsDataHashtable<ProcessIDHashKey, ScalarTupleArray> ScalarSnapshotTable;
+
+typedef mozilla::Tuple<const char*, nsTArray<KeyedScalar::KeyValuePair>,
+ uint32_t>
+ KeyedScalarDataTuple;
+typedef nsTArray<KeyedScalarDataTuple> KeyedScalarTupleArray;
+typedef nsDataHashtable<ProcessIDHashKey, KeyedScalarTupleArray>
+ KeyedScalarSnapshotTable;
+
+} // namespace
+
+////////////////////////////////////////////////////////////////////////
+////////////////////////////////////////////////////////////////////////
+//
+// PRIVATE STATE, SHARED BY ALL THREADS
+
+namespace {
+
+// Set to true once this global state has been initialized.
+bool gInitDone = false;
+
+bool gCanRecordBase;
+bool gCanRecordExtended;
+
+// The Name -> ID cache map.
+ScalarMapType gScalarNameIDMap(kScalarCount);
+
+// The (Process Id -> (Scalar ID -> Scalar Object)) map. This is a
+// nsClassHashtable, it owns the scalar instances and takes care of deallocating
+// them when they are removed from the map.
+ProcessesScalarsMapType gScalarStorageMap;
+// As above, for the keyed scalars.
+ProcessesKeyedScalarsMapType gKeyedScalarStorageMap;
+// Provide separate storage for "dynamic builtin" plain and keyed scalars,
+// needed to support "build faster" in local developer builds.
+ProcessesScalarsMapType gDynamicBuiltinScalarStorageMap;
+ProcessesKeyedScalarsMapType gDynamicBuiltinKeyedScalarStorageMap;
+
+// Whether or not the deserialization of persisted scalars is still in progress.
+// This is never the case on Desktop or Fennec.
+// Only GeckoView restores persisted scalars.
+bool gIsDeserializing = false;
+// This batches scalar accumulations that should be applied once loading
+// finished.
+StaticAutoPtr<nsTArray<ScalarAction>> gScalarsActions;
+StaticAutoPtr<nsTArray<KeyedScalarAction>> gKeyedScalarsActions;
+
+bool internal_IsScalarDeserializing(const StaticMutexAutoLock& lock) {
+ return gIsDeserializing;
+}
+
+} // namespace
+
+////////////////////////////////////////////////////////////////////////
+////////////////////////////////////////////////////////////////////////
+//
+// PRIVATE: Function that may call JS code.
+
+// NOTE: the functions in this section all run without protection from
+// |gTelemetryScalarsMutex|. If they held the mutex, there would be the
+// possibility of deadlock because the JS_ calls that they make may call
+// back into the TelemetryScalar 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 {
+
+/**
+ * Converts the error code to a human readable error message and prints it to
+ * the browser console.
+ *
+ * @param aScalarName The name of the scalar that raised the error.
+ * @param aSr The error code.
+ */
+void internal_LogScalarError(const nsACString& aScalarName, ScalarResult aSr) {
+ nsAutoString errorMessage;
+ AppendUTF8toUTF16(aScalarName, errorMessage);
+
+ switch (aSr) {
+ case ScalarResult::NotInitialized:
+ errorMessage.AppendLiteral(u" - Telemetry was not yet initialized.");
+ break;
+ case ScalarResult::CannotUnpackVariant:
+ errorMessage.AppendLiteral(
+ u" - Cannot convert the provided JS value to nsIVariant.");
+ break;
+ case ScalarResult::CannotRecordInProcess:
+ errorMessage.AppendLiteral(
+ u" - Cannot record the scalar in the current process.");
+ break;
+ case ScalarResult::KeyedTypeMismatch:
+ errorMessage.AppendLiteral(
+ u" - Attempting to manage a keyed scalar as a scalar (or "
+ u"vice-versa).");
+ break;
+ case ScalarResult::UnknownScalar:
+ errorMessage.AppendLiteral(u" - Unknown scalar.");
+ break;
+ case ScalarResult::OperationNotSupported:
+ errorMessage.AppendLiteral(
+ u" - The requested operation is not supported on this scalar.");
+ break;
+ case ScalarResult::InvalidType:
+ errorMessage.AppendLiteral(
+ u" - Attempted to set the scalar to an invalid data type.");
+ break;
+ case ScalarResult::InvalidValue:
+ errorMessage.AppendLiteral(
+ u" - Attempted to set the scalar to an incompatible value.");
+ break;
+ case ScalarResult::StringTooLong:
+ AppendUTF8toUTF16(
+ nsPrintfCString(" - Truncating scalar value to %d characters.",
+ kMaximumStringValueLength),
+ errorMessage);
+ break;
+ case ScalarResult::KeyIsEmpty:
+ errorMessage.AppendLiteral(u" - The key must not be empty.");
+ break;
+ case ScalarResult::KeyTooLong:
+ AppendUTF8toUTF16(
+ nsPrintfCString(" - The key length must be limited to %d characters.",
+ kMaximumKeyStringLength),
+ errorMessage);
+ break;
+ case ScalarResult::KeyNotAllowed:
+ AppendUTF8toUTF16(
+ nsPrintfCString(" - The key is not allowed for this scalar."),
+ errorMessage);
+ break;
+ case ScalarResult::TooManyKeys:
+ AppendUTF8toUTF16(
+ nsPrintfCString(" - Keyed scalars cannot have more than %d keys.",
+ kMaximumNumberOfKeys),
+ errorMessage);
+ break;
+ case ScalarResult::UnsignedNegativeValue:
+ errorMessage.AppendLiteral(
+ u" - Trying to set an unsigned scalar to a negative number.");
+ break;
+ case ScalarResult::UnsignedTruncatedValue:
+ errorMessage.AppendLiteral(u" - Truncating float/double number.");
+ break;
+ default:
+ // Nothing.
+ return;
+ }
+
+ LogToBrowserConsole(nsIScriptError::warningFlag, errorMessage);
+}
+
+} // namespace
+
+////////////////////////////////////////////////////////////////////////
+////////////////////////////////////////////////////////////////////////
+//
+// PRIVATE: helpers for the external interface
+
+namespace {
+
+bool internal_CanRecordBase(const StaticMutexAutoLock& lock) {
+ return gCanRecordBase;
+}
+
+bool internal_CanRecordExtended(const StaticMutexAutoLock& lock) {
+ return gCanRecordExtended;
+}
+
+/**
+ * Check if the given scalar is a keyed scalar.
+ *
+ * @param lock Instance of a lock locking gTelemetryHistogramMutex
+ * @param aId The scalar identifier.
+ * @return true if aId refers to a keyed scalar, false otherwise.
+ */
+bool internal_IsKeyedScalar(const StaticMutexAutoLock& lock,
+ const ScalarKey& aId) {
+ return internal_GetScalarInfo(lock, aId).keyed;
+}
+
+/**
+ * Check if we're allowed to record the given scalar in the current
+ * process.
+ *
+ * @param lock Instance of a lock locking gTelemetryHistogramMutex
+ * @param aId The scalar identifier.
+ * @return true if the scalar is allowed to be recorded in the current process,
+ * false otherwise.
+ */
+bool internal_CanRecordProcess(const StaticMutexAutoLock& lock,
+ const ScalarKey& aId) {
+ const BaseScalarInfo& info = internal_GetScalarInfo(lock, aId);
+ return CanRecordInProcess(info.record_in_processes, XRE_GetProcessType());
+}
+
+bool internal_CanRecordProduct(const StaticMutexAutoLock& lock,
+ const ScalarKey& aId) {
+ const BaseScalarInfo& info = internal_GetScalarInfo(lock, aId);
+ return CanRecordProduct(info.products);
+}
+
+bool internal_CanRecordForScalarID(const StaticMutexAutoLock& lock,
+ const ScalarKey& aId) {
+ // Get the scalar info from the id.
+ const BaseScalarInfo& info = internal_GetScalarInfo(lock, aId);
+
+ // Can we record at all?
+ bool canRecordBase = internal_CanRecordBase(lock);
+ if (!canRecordBase) {
+ return false;
+ }
+
+ bool canRecordDataset = CanRecordDataset(info.dataset, canRecordBase,
+ internal_CanRecordExtended(lock));
+ if (!canRecordDataset) {
+ return false;
+ }
+
+ return true;
+}
+
+/**
+ * Check if we are allowed to record the provided scalar.
+ *
+ * @param lock Instance of a lock locking gTelemetryHistogramMutex
+ * @param aId The scalar identifier.
+ * @param aKeyed Are we attempting to write a keyed scalar?
+ * @param aForce Whether to allow recording even if the probe is not allowed on
+ * the current process.
+ * This must only be true for GeckoView persistence and recorded
+ * actions.
+ * @return ScalarResult::Ok if we can record, an error code otherwise.
+ */
+ScalarResult internal_CanRecordScalar(const StaticMutexAutoLock& lock,
+ const ScalarKey& aId, bool aKeyed,
+ bool aForce = false) {
+ // Make sure that we have a keyed scalar if we are trying to change one.
+ if (internal_IsKeyedScalar(lock, aId) != aKeyed) {
+ return ScalarResult::KeyedTypeMismatch;
+ }
+
+ // Are we allowed to record this scalar based on the current Telemetry
+ // settings?
+ if (!internal_CanRecordForScalarID(lock, aId)) {
+ return ScalarResult::CannotRecordDataset;
+ }
+
+ // Can we record in this process?
+ if (!aForce && !internal_CanRecordProcess(lock, aId)) {
+ return ScalarResult::CannotRecordInProcess;
+ }
+
+ // Can we record on this product?
+ if (!internal_CanRecordProduct(lock, aId)) {
+ return ScalarResult::CannotRecordDataset;
+ }
+
+ return ScalarResult::Ok;
+}
+
+/**
+ * Get the scalar enum id from the scalar name.
+ *
+ * @param lock Instance of a lock locking gTelemetryHistogramMutex
+ * @param aName The scalar name.
+ * @param aId The output variable to contain the enum.
+ * @return
+ * NS_ERROR_FAILURE if this was called before init is completed.
+ * NS_ERROR_INVALID_ARG if the name can't be found in the scalar definitions.
+ * NS_OK if the scalar was found and aId contains a valid enum id.
+ */
+nsresult internal_GetEnumByScalarName(const StaticMutexAutoLock& lock,
+ const nsACString& aName, ScalarKey* aId) {
+ if (!gInitDone) {
+ return NS_ERROR_FAILURE;
+ }
+
+ CharPtrEntryType* entry =
+ gScalarNameIDMap.GetEntry(PromiseFlatCString(aName).get());
+ if (!entry) {
+ return NS_ERROR_INVALID_ARG;
+ }
+ *aId = entry->GetData();
+ return NS_OK;
+}
+
+/**
+ * Get a scalar object by its enum id. This implicitly allocates the scalar
+ * object in the storage if it wasn't previously allocated.
+ *
+ * @param lock Instance of a lock locking gTelemetryHistogramMutex
+ * @param aId The scalar identifier.
+ * @param aProcessStorage This drives the selection of the map to use to store
+ * the scalar data coming from child processes. This is only meaningful
+ * when this function is called in parent process. If that's the case,
+ * if this is not |GeckoProcessType_Default|, the process id is used to
+ * allocate and store the scalars.
+ * @param aRes The output variable that stores scalar object.
+ * @return
+ * NS_ERROR_INVALID_ARG if the scalar id is unknown.
+ * NS_ERROR_NOT_AVAILABLE if the scalar is expired.
+ * NS_OK if the scalar was found. If that's the case, aResult contains a
+ * valid pointer to a scalar type.
+ */
+nsresult internal_GetScalarByEnum(const StaticMutexAutoLock& lock,
+ const ScalarKey& aId,
+ ProcessID aProcessStorage,
+ ScalarBase** aRet) {
+ if (!internal_IsValidId(lock, aId)) {
+ MOZ_ASSERT(false, "Requested a scalar with an invalid id.");
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ const BaseScalarInfo& info = internal_GetScalarInfo(lock, aId);
+
+ // Dynamic scalars fixup: they are always stored in the "dynamic" process,
+ // unless they are part of the "builtin" Firefox probes. Please note that
+ // "dynamic builtin" probes are meant to support "artifact" and "build faster"
+ // builds.
+ if (aId.dynamic && !info.builtin) {
+ aProcessStorage = ProcessID::Dynamic;
+ }
+
+ ScalarBase* scalar = nullptr;
+ ScalarStorageMapType* scalarStorage = nullptr;
+ // Initialize the scalar storage to the parent storage. This will get
+ // set to the child storage if needed.
+ uint32_t storageId = static_cast<uint32_t>(aProcessStorage);
+
+ // Put dynamic-builtin scalars (used to support "build faster") in a
+ // separate storage.
+ ProcessesScalarsMapType& processStorage =
+ (aId.dynamic && info.builtin) ? gDynamicBuiltinScalarStorageMap
+ : gScalarStorageMap;
+
+ // Get the process-specific storage or create one if it's not
+ // available.
+ if (!processStorage.Get(storageId, &scalarStorage)) {
+ scalarStorage = new ScalarStorageMapType();
+ processStorage.Put(storageId, scalarStorage);
+ }
+
+ // Check if the scalar is already allocated in the parent or in the child
+ // storage.
+ if (scalarStorage->Get(aId.id, &scalar)) {
+ // Dynamic scalars can expire at any time during the session (e.g. an
+ // add-on was updated). Check if it expired.
+ if (aId.dynamic) {
+ const DynamicScalarInfo& dynInfo =
+ static_cast<const DynamicScalarInfo&>(info);
+ if (dynInfo.mDynamicExpiration) {
+ // The Dynamic scalar is expired.
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+ }
+ // This was not a dynamic scalar or was not expired.
+ *aRet = scalar;
+ return NS_OK;
+ }
+
+ // The scalar storage wasn't already allocated. Check if the scalar is expired
+ // and then allocate the storage, if needed.
+ if (IsExpiredVersion(info.expiration())) {
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+
+ scalar = internal_ScalarAllocate(info);
+ if (!scalar) {
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ scalarStorage->Put(aId.id, scalar);
+ *aRet = scalar;
+ return NS_OK;
+}
+
+void internal_ApplyPendingOperations(const StaticMutexAutoLock& lock);
+
+/**
+ * Record the given action on a scalar into the pending actions list.
+ *
+ * If the pending actions list overflows the high water mark length
+ * all operations are immediately applied, including the passed action.
+ *
+ * @param aScalarAction The action to record.
+ */
+void internal_RecordScalarAction(const StaticMutexAutoLock& lock,
+ const ScalarAction& aScalarAction) {
+ // Make sure to have the storage.
+ if (!gScalarsActions) {
+ gScalarsActions = new nsTArray<ScalarAction>();
+ }
+
+ // Store the action.
+ gScalarsActions->AppendElement(aScalarAction);
+
+ // If this action overflows the pending actions array, we immediately apply
+ // pending operations and assume loading is over. If loading still happens
+ // afterwards, some scalar values might be overwritten and inconsistent, but
+ // we won't lose operations on otherwise untouched probes.
+ if (gScalarsActions->Length() > kScalarActionsArrayHighWaterMark) {
+ internal_ApplyPendingOperations(lock);
+ return;
+ }
+}
+
+/**
+ * Record the given action on a scalar on the main process into the pending
+ * actions list.
+ *
+ * If the pending actions list overflows the high water mark length
+ * all operations are immediately applied, including the passed action.
+ *
+ * @param aId The scalar's ID this action applies to
+ * @param aDynamic Determines if the scalar is dynamic
+ * @param aAction The action to record
+ * @param aValue The additional data for the recorded action
+ */
+void internal_RecordScalarAction(const StaticMutexAutoLock& lock, uint32_t aId,
+ bool aDynamic, ScalarActionType aAction,
+ const ScalarVariant& aValue) {
+ internal_RecordScalarAction(
+ lock,
+ ScalarAction{aId, aDynamic, aAction, Some(aValue), ProcessID::Parent});
+}
+
+/**
+ * Record the given action on a keyed scalar into the pending actions list.
+ *
+ * If the pending actions list overflows the high water mark length
+ * all operations are immediately applied, including the passed action.
+ *
+ * @param aScalarAction The action to record.
+ */
+void internal_RecordKeyedScalarAction(const StaticMutexAutoLock& lock,
+ const KeyedScalarAction& aScalarAction) {
+ // Make sure to have the storage.
+ if (!gKeyedScalarsActions) {
+ gKeyedScalarsActions = new nsTArray<KeyedScalarAction>();
+ }
+
+ // Store the action.
+ gKeyedScalarsActions->AppendElement(aScalarAction);
+
+ // If this action overflows the pending actions array, we immediately apply
+ // pending operations and assume loading is over. If loading still happens
+ // afterwards, some scalar values might be overwritten and inconsistent, but
+ // we won't lose operations on otherwise untouched probes.
+ if (gKeyedScalarsActions->Length() > kScalarActionsArrayHighWaterMark) {
+ internal_ApplyPendingOperations(lock);
+ return;
+ }
+}
+
+/**
+ * Record the given action on a keyed scalar on the main process into the
+ * pending actions list.
+ *
+ * If the pending actions list overflows the high water mark length
+ * all operations are immediately applied, including the passed action.
+ *
+ * @param aId The scalar's ID this action applies to
+ * @param aDynamic Determines if the scalar is dynamic
+ * @param aKey The scalar's key
+ * @param aAction The action to record
+ * @param aValue The additional data for the recorded action
+ */
+void internal_RecordKeyedScalarAction(const StaticMutexAutoLock& lock,
+ uint32_t aId, bool aDynamic,
+ const nsAString& aKey,
+ ScalarActionType aAction,
+ const ScalarVariant& aValue) {
+ internal_RecordKeyedScalarAction(
+ lock,
+ KeyedScalarAction{aId, aDynamic, aAction, NS_ConvertUTF16toUTF8(aKey),
+ Some(aValue), ProcessID::Parent});
+}
+
+/**
+ * Update the scalar with the provided value. This is used by the JS API.
+ *
+ * @param lock Instance of a lock locking gTelemetryHistogramMutex
+ * @param aName The scalar name.
+ * @param aType The action type for updating the scalar.
+ * @param aValue The value to use for updating the scalar.
+ * @param aProcessOverride The process for which the scalar must be updated.
+ * This must only be used for GeckoView persistence. It must be
+ * set to the ProcessID::Parent for all the other cases.
+ * @param aForce Whether to force updating even if load is in progress.
+ * @return a ScalarResult error value.
+ */
+ScalarResult internal_UpdateScalar(
+ const StaticMutexAutoLock& lock, const nsACString& aName,
+ ScalarActionType aType, nsIVariant* aValue,
+ ProcessID aProcessOverride = ProcessID::Parent, bool aForce = false) {
+ ScalarKey uniqueId;
+ nsresult rv = internal_GetEnumByScalarName(lock, aName, &uniqueId);
+ if (NS_FAILED(rv)) {
+ return (rv == NS_ERROR_FAILURE) ? ScalarResult::NotInitialized
+ : ScalarResult::UnknownScalar;
+ }
+
+ ScalarResult sr = internal_CanRecordScalar(lock, uniqueId, false, aForce);
+ if (sr != ScalarResult::Ok) {
+ if (sr == ScalarResult::CannotRecordDataset) {
+ return ScalarResult::Ok;
+ }
+ return sr;
+ }
+
+ // Accumulate in the child process if needed.
+ if (!XRE_IsParentProcess()) {
+ const BaseScalarInfo& info = internal_GetScalarInfo(lock, uniqueId);
+ // Convert the nsIVariant to a Variant.
+ mozilla::Maybe<ScalarVariant> variantValue;
+ sr = GetVariantFromIVariant(aValue, info.kind, variantValue);
+ if (sr != ScalarResult::Ok) {
+ MOZ_ASSERT(false, "Unable to convert nsIVariant to mozilla::Variant.");
+ return sr;
+ }
+ TelemetryIPCAccumulator::RecordChildScalarAction(
+ uniqueId.id, uniqueId.dynamic, aType, variantValue.ref());
+ return ScalarResult::Ok;
+ }
+
+ if (!aForce && internal_IsScalarDeserializing(lock)) {
+ const BaseScalarInfo& info = internal_GetScalarInfo(lock, uniqueId);
+ // Convert the nsIVariant to a Variant.
+ mozilla::Maybe<ScalarVariant> variantValue;
+ sr = GetVariantFromIVariant(aValue, info.kind, variantValue);
+ if (sr != ScalarResult::Ok) {
+ MOZ_ASSERT(false, "Unable to convert nsIVariant to mozilla::Variant.");
+ return sr;
+ }
+ internal_RecordScalarAction(lock, uniqueId.id, uniqueId.dynamic, aType,
+ variantValue.ref());
+ return ScalarResult::Ok;
+ }
+
+ // Finally get the scalar.
+ ScalarBase* scalar = nullptr;
+ rv = internal_GetScalarByEnum(lock, uniqueId, aProcessOverride, &scalar);
+ if (NS_FAILED(rv)) {
+ // Don't throw on expired scalars.
+ if (rv == NS_ERROR_NOT_AVAILABLE) {
+ return ScalarResult::Ok;
+ }
+ return ScalarResult::UnknownScalar;
+ }
+
+ if (aType == ScalarActionType::eAdd) {
+ return scalar->AddValue(aValue);
+ }
+ if (aType == ScalarActionType::eSet) {
+ return scalar->SetValue(aValue);
+ }
+
+ return scalar->SetMaximum(aValue);
+}
+
+} // namespace
+
+////////////////////////////////////////////////////////////////////////
+////////////////////////////////////////////////////////////////////////
+//
+// PRIVATE: thread-unsafe helpers for the keyed scalars
+
+namespace {
+
+/**
+ * Get a keyed scalar object by its enum id. This implicitly allocates the keyed
+ * scalar object in the storage if it wasn't previously allocated.
+ *
+ * @param lock Instance of a lock locking gTelemetryHistogramMutex
+ * @param aId The scalar identifier.
+ * @param aProcessStorage This drives the selection of the map to use to store
+ * the scalar data coming from child processes. This is only meaningful
+ * when this function is called in parent process. If that's the case,
+ * if this is not |GeckoProcessType_Default|, the process id is used to
+ * allocate and store the scalars.
+ * @param aRet The output variable that stores scalar object.
+ * @return
+ * NS_ERROR_INVALID_ARG if the scalar id is unknown or a this is a keyed
+ * string scalar.
+ * NS_ERROR_NOT_AVAILABLE if the scalar is expired.
+ * NS_OK if the scalar was found. If that's the case, aResult contains a
+ * valid pointer to a scalar type.
+ */
+nsresult internal_GetKeyedScalarByEnum(const StaticMutexAutoLock& lock,
+ const ScalarKey& aId,
+ ProcessID aProcessStorage,
+ KeyedScalar** aRet) {
+ if (!internal_IsValidId(lock, aId)) {
+ MOZ_ASSERT(false, "Requested a keyed scalar with an invalid id.");
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ const BaseScalarInfo& info = internal_GetScalarInfo(lock, aId);
+
+ // Dynamic scalars fixup: they are always stored in the "dynamic" process,
+ // unless they are part of the "builtin" Firefox probes. Please note that
+ // "dynamic builtin" probes are meant to support "artifact" and "build faster"
+ // builds.
+ if (aId.dynamic && !info.builtin) {
+ aProcessStorage = ProcessID::Dynamic;
+ }
+
+ KeyedScalar* scalar = nullptr;
+ KeyedScalarStorageMapType* scalarStorage = nullptr;
+ // Initialize the scalar storage to the parent storage. This will get
+ // set to the child storage if needed.
+ uint32_t storageId = static_cast<uint32_t>(aProcessStorage);
+
+ // Put dynamic-builtin scalars (used to support "build faster") in a
+ // separate storage.
+ ProcessesKeyedScalarsMapType& processStorage =
+ (aId.dynamic && info.builtin) ? gDynamicBuiltinKeyedScalarStorageMap
+ : gKeyedScalarStorageMap;
+
+ // Get the process-specific storage or create one if it's not
+ // available.
+ if (!processStorage.Get(storageId, &scalarStorage)) {
+ scalarStorage = new KeyedScalarStorageMapType();
+ processStorage.Put(storageId, scalarStorage);
+ }
+
+ if (scalarStorage->Get(aId.id, &scalar)) {
+ *aRet = scalar;
+ return NS_OK;
+ }
+
+ if (IsExpiredVersion(info.expiration())) {
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+
+ // We don't currently support keyed string scalars. Disable them.
+ if (info.kind == nsITelemetry::SCALAR_TYPE_STRING) {
+ MOZ_ASSERT(false, "Keyed string scalars are not currently supported.");
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ scalar = new KeyedScalar(info);
+ if (!scalar) {
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ scalarStorage->Put(aId.id, scalar);
+ *aRet = scalar;
+ return NS_OK;
+}
+
+/**
+ * Update the keyed scalar with the provided value. This is used by the JS API.
+ *
+ * @param lock Instance of a lock locking gTelemetryHistogramMutex
+ * @param aName The scalar name.
+ * @param aKey The key name.
+ * @param aType The action type for updating the scalar.
+ * @param aValue The value to use for updating the scalar.
+ * @param aProcessOverride The process for which the scalar must be updated.
+ * This must only be used for GeckoView persistence. It must be
+ * set to the ProcessID::Parent for all the other cases.
+ * @return a ScalarResult error value.
+ */
+ScalarResult internal_UpdateKeyedScalar(
+ const StaticMutexAutoLock& lock, const nsACString& aName,
+ const nsAString& aKey, ScalarActionType aType, nsIVariant* aValue,
+ ProcessID aProcessOverride = ProcessID::Parent, bool aForce = false) {
+ ScalarKey uniqueId;
+ nsresult rv = internal_GetEnumByScalarName(lock, aName, &uniqueId);
+ if (NS_FAILED(rv)) {
+ return (rv == NS_ERROR_FAILURE) ? ScalarResult::NotInitialized
+ : ScalarResult::UnknownScalar;
+ }
+
+ ScalarResult sr = internal_CanRecordScalar(lock, uniqueId, true, aForce);
+ if (sr != ScalarResult::Ok) {
+ if (sr == ScalarResult::CannotRecordDataset) {
+ return ScalarResult::Ok;
+ }
+ return sr;
+ }
+
+ // Accumulate in the child process if needed.
+ if (!XRE_IsParentProcess()) {
+ const BaseScalarInfo& info = internal_GetScalarInfo(lock, uniqueId);
+ // Convert the nsIVariant to a Variant.
+ mozilla::Maybe<ScalarVariant> variantValue;
+ sr = GetVariantFromIVariant(aValue, info.kind, variantValue);
+ if (sr != ScalarResult::Ok) {
+ MOZ_ASSERT(false, "Unable to convert nsIVariant to mozilla::Variant.");
+ return sr;
+ }
+ TelemetryIPCAccumulator::RecordChildKeyedScalarAction(
+ uniqueId.id, uniqueId.dynamic, aKey, aType, variantValue.ref());
+ return ScalarResult::Ok;
+ }
+
+ if (!aForce && internal_IsScalarDeserializing(lock)) {
+ const BaseScalarInfo& info = internal_GetScalarInfo(lock, uniqueId);
+ // Convert the nsIVariant to a Variant.
+ mozilla::Maybe<ScalarVariant> variantValue;
+ sr = GetVariantFromIVariant(aValue, info.kind, variantValue);
+ if (sr != ScalarResult::Ok) {
+ MOZ_ASSERT(false, "Unable to convert nsIVariant to mozilla::Variant.");
+ return sr;
+ }
+ internal_RecordKeyedScalarAction(lock, uniqueId.id, uniqueId.dynamic, aKey,
+ aType, variantValue.ref());
+ return ScalarResult::Ok;
+ }
+
+ // Finally get the scalar.
+ KeyedScalar* scalar = nullptr;
+ rv = internal_GetKeyedScalarByEnum(lock, uniqueId, aProcessOverride, &scalar);
+ if (NS_FAILED(rv)) {
+ // Don't throw on expired scalars.
+ if (rv == NS_ERROR_NOT_AVAILABLE) {
+ return ScalarResult::Ok;
+ }
+ return ScalarResult::UnknownScalar;
+ }
+
+ if (aType == ScalarActionType::eAdd) {
+ return scalar->AddValue(lock, aKey, aValue);
+ }
+ if (aType == ScalarActionType::eSet) {
+ return scalar->SetValue(lock, aKey, aValue);
+ }
+
+ return scalar->SetMaximum(lock, aKey, aValue);
+}
+
+/**
+ * Helper function to convert an array of |DynamicScalarInfo|
+ * to |DynamicScalarDefinition| used by the IPC calls.
+ */
+void internal_DynamicScalarToIPC(
+ const StaticMutexAutoLock& lock,
+ const nsTArray<DynamicScalarInfo>& aDynamicScalarInfos,
+ nsTArray<DynamicScalarDefinition>& aIPCDefs) {
+ for (auto& info : aDynamicScalarInfos) {
+ DynamicScalarDefinition stubDefinition;
+ stubDefinition.type = info.kind;
+ stubDefinition.dataset = info.dataset;
+ stubDefinition.expired = info.mDynamicExpiration;
+ stubDefinition.keyed = info.keyed;
+ stubDefinition.name = info.mDynamicName;
+ stubDefinition.builtin = info.builtin;
+ aIPCDefs.AppendElement(stubDefinition);
+ }
+}
+
+/**
+ * Broadcasts the dynamic scalar definitions to all the other
+ * content processes.
+ */
+void internal_BroadcastDefinitions(
+ const nsTArray<DynamicScalarDefinition>& scalarDefs) {
+ nsTArray<mozilla::dom::ContentParent*> parents;
+ mozilla::dom::ContentParent::GetAll(parents);
+ if (!parents.Length()) {
+ return;
+ }
+
+ // Broadcast the definitions to the other content processes.
+ for (auto parent : parents) {
+ mozilla::Unused << parent->SendAddDynamicScalars(scalarDefs);
+ }
+}
+
+void internal_RegisterScalars(const StaticMutexAutoLock& lock,
+ const nsTArray<DynamicScalarInfo>& scalarInfos) {
+ // Register the new scalars.
+ if (!gDynamicScalarInfo) {
+ gDynamicScalarInfo = new nsTArray<DynamicScalarInfo>();
+ }
+ if (!gDynamicStoreNames) {
+ gDynamicStoreNames = new nsTArray<RefPtr<nsAtom>>();
+ }
+
+ for (auto& scalarInfo : scalarInfos) {
+ // Allow expiring scalars that were already registered.
+ CharPtrEntryType* existingKey =
+ gScalarNameIDMap.GetEntry(scalarInfo.name());
+ if (existingKey) {
+ // Change the scalar to expired if needed.
+ if (scalarInfo.mDynamicExpiration && !scalarInfo.builtin) {
+ DynamicScalarInfo& scalarData =
+ (*gDynamicScalarInfo)[existingKey->GetData().id];
+ scalarData.mDynamicExpiration = true;
+ }
+ continue;
+ }
+
+ gDynamicScalarInfo->AppendElement(scalarInfo);
+ uint32_t scalarId = gDynamicScalarInfo->Length() - 1;
+ CharPtrEntryType* entry = gScalarNameIDMap.PutEntry(scalarInfo.name());
+ entry->SetData(ScalarKey{scalarId, true});
+ }
+}
+
+/**
+ * Creates a snapshot of the desired scalar storage.
+ * @param {aLock} The proof of lock to access scalar data.
+ * @param {aScalarsToReflect} The table that will contain the snapshot.
+ * @param {aDataset} The dataset we're asking the snapshot for.
+ * @param {aProcessStorage} The scalar storage to take a snapshot of.
+ * @param {aIsBuiltinDynamic} Whether or not the storage is for dynamic builtin
+ * scalars.
+ * @return NS_OK or the error code describing the failure reason.
+ */
+nsresult internal_ScalarSnapshotter(const StaticMutexAutoLock& aLock,
+ ScalarSnapshotTable& aScalarsToReflect,
+ unsigned int aDataset,
+ ProcessesScalarsMapType& aProcessStorage,
+ bool aIsBuiltinDynamic, bool aClearScalars,
+ const nsACString& aStoreName) {
+ // Iterate the scalars in aProcessStorage. The storage may contain empty or
+ // yet to be initialized scalars from all the supported processes.
+ for (auto iter = aProcessStorage.Iter(); !iter.Done(); iter.Next()) {
+ ScalarStorageMapType* scalarStorage = iter.UserData();
+ ScalarTupleArray& processScalars =
+ aScalarsToReflect.GetOrInsert(iter.Key());
+
+ // Are we in the "Dynamic" process?
+ bool isDynamicProcess =
+ ProcessID::Dynamic == static_cast<ProcessID>(iter.Key());
+
+ // Iterate each available child storage.
+ for (auto childIter = scalarStorage->Iter(); !childIter.Done();
+ childIter.Next()) {
+ ScalarBase* scalar = childIter.UserData();
+
+ // Get the informations for this scalar.
+ const BaseScalarInfo& info = internal_GetScalarInfo(
+ aLock, ScalarKey{childIter.Key(),
+ aIsBuiltinDynamic ? true : isDynamicProcess});
+
+ // Serialize the scalar if it's in the desired dataset.
+ if (IsInDataset(info.dataset, aDataset)) {
+ // Get the scalar value.
+ nsCOMPtr<nsIVariant> scalarValue;
+ nsresult rv = scalar->GetValue(aStoreName, aClearScalars, scalarValue);
+ if (rv == NS_ERROR_NO_CONTENT) {
+ // No value for this store. Proceed.
+ continue;
+ }
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+ // Append it to our list.
+ processScalars.AppendElement(
+ mozilla::MakeTuple(info.name(), scalarValue, info.kind));
+ }
+ }
+ if (processScalars.Length() == 0) {
+ aScalarsToReflect.Remove(iter.Key());
+ }
+ }
+ return NS_OK;
+}
+
+/**
+ * Creates a snapshot of the desired keyed scalar storage.
+ * @param {aLock} The proof of lock to access scalar data.
+ * @param {aScalarsToReflect} The table that will contain the snapshot.
+ * @param {aDataset} The dataset we're asking the snapshot for.
+ * @param {aProcessStorage} The scalar storage to take a snapshot of.
+ * @param {aIsBuiltinDynamic} Whether or not the storage is for dynamic builtin
+ * scalars.
+ * @return NS_OK or the error code describing the failure reason.
+ */
+nsresult internal_KeyedScalarSnapshotter(
+ const StaticMutexAutoLock& aLock,
+ KeyedScalarSnapshotTable& aScalarsToReflect, unsigned int aDataset,
+ ProcessesKeyedScalarsMapType& aProcessStorage, bool aIsBuiltinDynamic,
+ bool aClearScalars, const nsACString& aStoreName) {
+ // Iterate the scalars in aProcessStorage. The storage may contain empty or
+ // yet to be initialized scalars from all the supported processes.
+ for (auto iter = aProcessStorage.Iter(); !iter.Done(); iter.Next()) {
+ KeyedScalarStorageMapType* scalarStorage = iter.UserData();
+ KeyedScalarTupleArray& processScalars =
+ aScalarsToReflect.GetOrInsert(iter.Key());
+
+ // Are we in the "Dynamic" process?
+ bool isDynamicProcess =
+ ProcessID::Dynamic == static_cast<ProcessID>(iter.Key());
+
+ for (auto childIter = scalarStorage->Iter(); !childIter.Done();
+ childIter.Next()) {
+ KeyedScalar* scalar = childIter.UserData();
+
+ // Get the informations for this scalar.
+ const BaseScalarInfo& info = internal_GetScalarInfo(
+ aLock, ScalarKey{childIter.Key(),
+ aIsBuiltinDynamic ? true : isDynamicProcess});
+
+ // Serialize the scalar if it's in the desired dataset.
+ if (IsInDataset(info.dataset, aDataset)) {
+ // Get the keys for this scalar.
+ nsTArray<KeyedScalar::KeyValuePair> scalarKeyedData;
+ nsresult rv =
+ scalar->GetValue(aStoreName, aClearScalars, scalarKeyedData);
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+ if (scalarKeyedData.Length() == 0) {
+ // Don't bother with empty keyed scalars.
+ continue;
+ }
+ // Append it to our list.
+ processScalars.AppendElement(mozilla::MakeTuple(
+ info.name(), std::move(scalarKeyedData), info.kind));
+ }
+ }
+ if (processScalars.Length() == 0) {
+ aScalarsToReflect.Remove(iter.Key());
+ }
+ }
+ return NS_OK;
+}
+
+/**
+ * Helper function to get a snapshot of the scalars.
+ *
+ * @param {aLock} The proof of lock to access scalar data.
+ * @param {aScalarsToReflect} The table that will contain the snapshot.
+ * @param {aDataset} The dataset we're asking the snapshot for.
+ * @param {aClearScalars} Whether or not to clear the scalar storage.
+ * @param {aStoreName} The name of the store to snapshot.
+ * @return NS_OK or the error code describing the failure reason.
+ */
+nsresult internal_GetScalarSnapshot(const StaticMutexAutoLock& aLock,
+ ScalarSnapshotTable& aScalarsToReflect,
+ unsigned int aDataset, bool aClearScalars,
+ const nsACString& aStoreName) {
+ // Take a snapshot of the scalars.
+ nsresult rv =
+ internal_ScalarSnapshotter(aLock, aScalarsToReflect, aDataset,
+ gScalarStorageMap, false, /*aIsBuiltinDynamic*/
+ aClearScalars, aStoreName);
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+
+ // And a snapshot of the dynamic builtin ones.
+ rv = internal_ScalarSnapshotter(aLock, aScalarsToReflect, aDataset,
+ gDynamicBuiltinScalarStorageMap,
+ true, /*aIsBuiltinDynamic*/
+ aClearScalars, aStoreName);
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+
+ return NS_OK;
+}
+
+/**
+ * Helper function to get a snapshot of the keyed scalars.
+ *
+ * @param {aLock} The proof of lock to access scalar data.
+ * @param {aScalarsToReflect} The table that will contain the snapshot.
+ * @param {aDataset} The dataset we're asking the snapshot for.
+ * @param {aClearScalars} Whether or not to clear the scalar storage.
+ * @param {aStoreName} The name of the store to snapshot.
+ * @return NS_OK or the error code describing the failure reason.
+ */
+nsresult internal_GetKeyedScalarSnapshot(
+ const StaticMutexAutoLock& aLock,
+ KeyedScalarSnapshotTable& aScalarsToReflect, unsigned int aDataset,
+ bool aClearScalars, const nsACString& aStoreName) {
+ // Take a snapshot of the scalars.
+ nsresult rv = internal_KeyedScalarSnapshotter(
+ aLock, aScalarsToReflect, aDataset, gKeyedScalarStorageMap,
+ false, /*aIsBuiltinDynamic*/
+ aClearScalars, aStoreName);
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+
+ // And a snapshot of the dynamic builtin ones.
+ rv = internal_KeyedScalarSnapshotter(aLock, aScalarsToReflect, aDataset,
+ gDynamicBuiltinKeyedScalarStorageMap,
+ true, /*aIsBuiltinDynamic*/
+ aClearScalars, aStoreName);
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+
+ return NS_OK;
+}
+
+} // namespace
+
+// helpers for recording/applying scalar operations
+namespace {
+
+void internal_ApplyScalarActions(
+ const StaticMutexAutoLock& lock,
+ const nsTArray<mozilla::Telemetry::ScalarAction>& aScalarActions,
+ const mozilla::Maybe<ProcessID>& aProcessType = Nothing()) {
+ if (!internal_CanRecordBase(lock)) {
+ return;
+ }
+
+ for (auto& upd : aScalarActions) {
+ ScalarKey uniqueId{upd.mId, upd.mDynamic};
+ if (NS_WARN_IF(!internal_IsValidId(lock, uniqueId))) {
+ MOZ_ASSERT_UNREACHABLE("Scalar usage requires valid ids.");
+ continue;
+ }
+
+ if (internal_IsKeyedScalar(lock, uniqueId)) {
+ continue;
+ }
+
+ // Are we allowed to record this scalar? We don't need to check for
+ // allowed processes here, that's taken care of when recording
+ // in child processes.
+ if (!internal_CanRecordForScalarID(lock, uniqueId)) {
+ continue;
+ }
+
+ // Either we got passed a process type or it was explicitely set on the
+ // recorded action. It should never happen that it is set to an invalid
+ // value (such as ProcessID::Count)
+ ProcessID processType = aProcessType.valueOr(upd.mProcessType);
+ MOZ_ASSERT(processType != ProcessID::Count);
+
+ // Refresh the data in the parent process with the data coming from the
+ // child processes.
+ ScalarBase* scalar = nullptr;
+ nsresult rv =
+ internal_GetScalarByEnum(lock, uniqueId, processType, &scalar);
+ if (NS_FAILED(rv)) {
+ // Bug 1513496 - We no longer log a warning if the scalar is expired.
+ if (rv != NS_ERROR_NOT_AVAILABLE) {
+ NS_WARNING("NS_FAILED internal_GetScalarByEnum for CHILD");
+ }
+ continue;
+ }
+
+ if (upd.mData.isNothing()) {
+ MOZ_ASSERT(false, "There is no data in the ScalarActionType.");
+ continue;
+ }
+
+ // Get the type of this scalar from the scalar ID. We already checked
+ // for its validity a few lines above.
+ const uint32_t scalarType = internal_GetScalarInfo(lock, uniqueId).kind;
+
+ // Extract the data from the mozilla::Variant.
+ switch (upd.mActionType) {
+ case ScalarActionType::eSet: {
+ switch (scalarType) {
+ case nsITelemetry::SCALAR_TYPE_COUNT:
+ if (!upd.mData->is<uint32_t>()) {
+ NS_WARNING("Attempting to set a count scalar to a non-integer.");
+ continue;
+ }
+ scalar->SetValue(upd.mData->as<uint32_t>());
+ break;
+ case nsITelemetry::SCALAR_TYPE_BOOLEAN:
+ if (!upd.mData->is<bool>()) {
+ NS_WARNING(
+ "Attempting to set a boolean scalar to a non-boolean.");
+ continue;
+ }
+ scalar->SetValue(upd.mData->as<bool>());
+ break;
+ case nsITelemetry::SCALAR_TYPE_STRING:
+ if (!upd.mData->is<nsString>()) {
+ NS_WARNING("Attempting to set a string scalar to a non-string.");
+ continue;
+ }
+ scalar->SetValue(upd.mData->as<nsString>());
+ break;
+ }
+ break;
+ }
+ case ScalarActionType::eAdd: {
+ if (scalarType != nsITelemetry::SCALAR_TYPE_COUNT) {
+ NS_WARNING("Attempting to add on a non count scalar.");
+ continue;
+ }
+ // We only support adding uint32_t.
+ if (!upd.mData->is<uint32_t>()) {
+ NS_WARNING("Attempting to add to a count scalar with a non-integer.");
+ continue;
+ }
+ scalar->AddValue(upd.mData->as<uint32_t>());
+ break;
+ }
+ case ScalarActionType::eSetMaximum: {
+ if (scalarType != nsITelemetry::SCALAR_TYPE_COUNT) {
+ NS_WARNING("Attempting to setMaximum on a non count scalar.");
+ continue;
+ }
+ // We only support SetMaximum on uint32_t.
+ if (!upd.mData->is<uint32_t>()) {
+ NS_WARNING(
+ "Attempting to setMaximum a count scalar to a non-integer.");
+ continue;
+ }
+ scalar->SetMaximum(upd.mData->as<uint32_t>());
+ break;
+ }
+ default:
+ NS_WARNING("Unsupported action coming from scalar child updates.");
+ }
+ }
+}
+
+void internal_ApplyKeyedScalarActions(
+ const StaticMutexAutoLock& lock,
+ const nsTArray<mozilla::Telemetry::KeyedScalarAction>& aScalarActions,
+ const mozilla::Maybe<ProcessID>& aProcessType = Nothing()) {
+ if (!internal_CanRecordBase(lock)) {
+ return;
+ }
+
+ for (auto& upd : aScalarActions) {
+ ScalarKey uniqueId{upd.mId, upd.mDynamic};
+ if (NS_WARN_IF(!internal_IsValidId(lock, uniqueId))) {
+ MOZ_ASSERT_UNREACHABLE("Scalar usage requires valid ids.");
+ continue;
+ }
+
+ if (!internal_IsKeyedScalar(lock, uniqueId)) {
+ continue;
+ }
+
+ // Are we allowed to record this scalar? We don't need to check for
+ // allowed processes here, that's taken care of when recording
+ // in child processes.
+ if (!internal_CanRecordForScalarID(lock, uniqueId)) {
+ continue;
+ }
+
+ // Either we got passed a process type or it was explicitely set on the
+ // recorded action. It should never happen that it is set to an invalid
+ // value (such as ProcessID::Count)
+ ProcessID processType = aProcessType.valueOr(upd.mProcessType);
+ MOZ_ASSERT(processType != ProcessID::Count);
+
+ // Refresh the data in the parent process with the data coming from the
+ // child processes.
+ KeyedScalar* scalar = nullptr;
+ nsresult rv =
+ internal_GetKeyedScalarByEnum(lock, uniqueId, processType, &scalar);
+ if (NS_FAILED(rv)) {
+ // Bug 1513496 - We no longer log a warning if the scalar is expired.
+ if (rv != NS_ERROR_NOT_AVAILABLE) {
+ NS_WARNING("NS_FAILED internal_GetKeyedScalarByEnum for CHILD");
+ }
+ continue;
+ }
+
+ if (upd.mData.isNothing()) {
+ MOZ_ASSERT(false, "There is no data in the KeyedScalarAction.");
+ continue;
+ }
+
+ // Get the type of this scalar from the scalar ID. We already checked
+ // for its validity a few lines above.
+ const uint32_t scalarType = internal_GetScalarInfo(lock, uniqueId).kind;
+
+ // Extract the data from the mozilla::Variant.
+ switch (upd.mActionType) {
+ case ScalarActionType::eSet: {
+ switch (scalarType) {
+ case nsITelemetry::SCALAR_TYPE_COUNT:
+ if (!upd.mData->is<uint32_t>()) {
+ NS_WARNING("Attempting to set a count scalar to a non-integer.");
+ continue;
+ }
+ scalar->SetValue(lock, NS_ConvertUTF8toUTF16(upd.mKey),
+ upd.mData->as<uint32_t>());
+ break;
+ case nsITelemetry::SCALAR_TYPE_BOOLEAN:
+ if (!upd.mData->is<bool>()) {
+ NS_WARNING(
+ "Attempting to set a boolean scalar to a non-boolean.");
+ continue;
+ }
+ scalar->SetValue(lock, NS_ConvertUTF8toUTF16(upd.mKey),
+ upd.mData->as<bool>());
+ break;
+ default:
+ NS_WARNING("Unsupported type coming from scalar child updates.");
+ }
+ break;
+ }
+ case ScalarActionType::eAdd: {
+ if (scalarType != nsITelemetry::SCALAR_TYPE_COUNT) {
+ NS_WARNING("Attempting to add on a non count scalar.");
+ continue;
+ }
+ // We only support adding on uint32_t.
+ if (!upd.mData->is<uint32_t>()) {
+ NS_WARNING("Attempting to add to a count scalar with a non-integer.");
+ continue;
+ }
+ scalar->AddValue(lock, NS_ConvertUTF8toUTF16(upd.mKey),
+ upd.mData->as<uint32_t>());
+ break;
+ }
+ case ScalarActionType::eSetMaximum: {
+ if (scalarType != nsITelemetry::SCALAR_TYPE_COUNT) {
+ NS_WARNING("Attempting to setMaximum on a non count scalar.");
+ continue;
+ }
+ // We only support SetMaximum on uint32_t.
+ if (!upd.mData->is<uint32_t>()) {
+ NS_WARNING(
+ "Attempting to setMaximum a count scalar to a non-integer.");
+ continue;
+ }
+ scalar->SetMaximum(lock, NS_ConvertUTF8toUTF16(upd.mKey),
+ upd.mData->as<uint32_t>());
+ break;
+ }
+ default:
+ NS_WARNING(
+ "Unsupported action coming from keyed scalar child updates.");
+ }
+ }
+}
+
+void internal_ApplyPendingOperations(const StaticMutexAutoLock& lock) {
+ if (gScalarsActions && gScalarsActions->Length() > 0) {
+ internal_ApplyScalarActions(lock, *gScalarsActions);
+ gScalarsActions->Clear();
+ }
+
+ if (gKeyedScalarsActions && gKeyedScalarsActions->Length() > 0) {
+ internal_ApplyKeyedScalarActions(lock, *gKeyedScalarsActions);
+ gKeyedScalarsActions->Clear();
+ }
+
+ // After all pending operations are applied deserialization is done
+ gIsDeserializing = false;
+}
+
+} // namespace
+
+////////////////////////////////////////////////////////////////////////
+////////////////////////////////////////////////////////////////////////
+//
+// EXTERNALLY VISIBLE FUNCTIONS in namespace TelemetryScalars::
+
+// 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.
+// Another reason to use a StaticMutex instead of a plain Mutex is
+// that, due to the nature of Telemetry, we cannot rely on having a
+// mutex initialized in InitializeGlobalState. Unfortunately, we
+// cannot make sure that no other function is called before this point.
+static StaticMutex gTelemetryScalarsMutex;
+
+void TelemetryScalar::InitializeGlobalState(bool aCanRecordBase,
+ bool aCanRecordExtended) {
+ StaticMutexAutoLock locker(gTelemetryScalarsMutex);
+ MOZ_ASSERT(!gInitDone,
+ "TelemetryScalar::InitializeGlobalState "
+ "may only be called once");
+
+ gCanRecordBase = aCanRecordBase;
+ gCanRecordExtended = aCanRecordExtended;
+
+ // Populate the static scalar name->id cache. Note that the scalar names are
+ // statically allocated and come from the automatically generated
+ // TelemetryScalarData.h.
+ uint32_t scalarCount =
+ static_cast<uint32_t>(mozilla::Telemetry::ScalarID::ScalarCount);
+ for (uint32_t i = 0; i < scalarCount; i++) {
+ CharPtrEntryType* entry = gScalarNameIDMap.PutEntry(gScalars[i].name());
+ entry->SetData(ScalarKey{i, false});
+ }
+
+ // To summarize dynamic events we need a dynamic scalar.
+ const nsTArray<DynamicScalarInfo> initialDynamicScalars({
+ DynamicScalarInfo{
+ nsITelemetry::SCALAR_TYPE_COUNT,
+ true /* recordOnRelease */,
+ false /* expired */,
+ nsAutoCString("telemetry.dynamic_event_counts"),
+ true /* keyed */,
+ false /* built-in */,
+ {} /* stores */,
+ },
+ });
+ internal_RegisterScalars(locker, initialDynamicScalars);
+
+ gInitDone = true;
+}
+
+void TelemetryScalar::DeInitializeGlobalState() {
+ StaticMutexAutoLock locker(gTelemetryScalarsMutex);
+ gCanRecordBase = false;
+ gCanRecordExtended = false;
+ gScalarNameIDMap.Clear();
+ gScalarStorageMap.Clear();
+ gKeyedScalarStorageMap.Clear();
+ gDynamicBuiltinScalarStorageMap.Clear();
+ gDynamicBuiltinKeyedScalarStorageMap.Clear();
+ gDynamicScalarInfo = nullptr;
+ gDynamicStoreNames = nullptr;
+ gInitDone = false;
+}
+
+void TelemetryScalar::DeserializationStarted() {
+ StaticMutexAutoLock locker(gTelemetryScalarsMutex);
+ gIsDeserializing = true;
+}
+
+void TelemetryScalar::ApplyPendingOperations() {
+ StaticMutexAutoLock locker(gTelemetryScalarsMutex);
+ internal_ApplyPendingOperations(locker);
+}
+
+void TelemetryScalar::SetCanRecordBase(bool b) {
+ StaticMutexAutoLock locker(gTelemetryScalarsMutex);
+ gCanRecordBase = b;
+}
+
+void TelemetryScalar::SetCanRecordExtended(bool b) {
+ StaticMutexAutoLock locker(gTelemetryScalarsMutex);
+ gCanRecordExtended = b;
+}
+
+/**
+ * Adds the value to the given scalar.
+ *
+ * @param aName The scalar name.
+ * @param aVal The numeric value to add to the scalar.
+ * @param aCx The JS context.
+ * @return NS_OK (always) so that the JS API call doesn't throw. In case of
+ * errors, a warning level message is printed in the browser console.
+ */
+nsresult TelemetryScalar::Add(const nsACString& aName, JS::HandleValue aVal,
+ JSContext* aCx) {
+ // Unpack the aVal to nsIVariant. This uses the JS context.
+ nsCOMPtr<nsIVariant> unpackedVal;
+ nsresult rv = nsContentUtils::XPConnect()->JSToVariant(
+ aCx, aVal, getter_AddRefs(unpackedVal));
+ if (NS_FAILED(rv)) {
+ internal_LogScalarError(aName, ScalarResult::CannotUnpackVariant);
+ return NS_OK;
+ }
+
+ ScalarResult sr;
+ {
+ StaticMutexAutoLock locker(gTelemetryScalarsMutex);
+ sr = internal_UpdateScalar(locker, aName, ScalarActionType::eAdd,
+ unpackedVal);
+ }
+
+ // Warn the user about the error if we need to.
+ if (sr != ScalarResult::Ok) {
+ internal_LogScalarError(aName, sr);
+ }
+
+ return NS_OK;
+}
+
+/**
+ * Adds the value to the given scalar.
+ *
+ * @param aName The scalar name.
+ * @param aKey The key name.
+ * @param aVal The numeric value to add to the scalar.
+ * @param aCx The JS context.
+ * @return NS_OK (always) so that the JS API call doesn't throw. In case of
+ * errors, a warning level message is printed in the browser console.
+ */
+nsresult TelemetryScalar::Add(const nsACString& aName, const nsAString& aKey,
+ JS::HandleValue aVal, JSContext* aCx) {
+ // Unpack the aVal to nsIVariant. This uses the JS context.
+ nsCOMPtr<nsIVariant> unpackedVal;
+ nsresult rv = nsContentUtils::XPConnect()->JSToVariant(
+ aCx, aVal, getter_AddRefs(unpackedVal));
+ if (NS_FAILED(rv)) {
+ internal_LogScalarError(aName, ScalarResult::CannotUnpackVariant);
+ return NS_OK;
+ }
+
+ ScalarResult sr;
+ {
+ StaticMutexAutoLock locker(gTelemetryScalarsMutex);
+ sr = internal_UpdateKeyedScalar(locker, aName, aKey, ScalarActionType::eAdd,
+ unpackedVal);
+ }
+
+ // Warn the user about the error if we need to.
+ if (sr != ScalarResult::Ok) {
+ internal_LogScalarError(aName, sr);
+ }
+
+ return NS_OK;
+}
+
+/**
+ * Adds the value to the given scalar.
+ *
+ * @param aId The scalar enum id.
+ * @param aVal The numeric value to add to the scalar.
+ */
+void TelemetryScalar::Add(mozilla::Telemetry::ScalarID aId, uint32_t aValue) {
+ if (NS_WARN_IF(!IsValidEnumId(aId))) {
+ MOZ_ASSERT_UNREACHABLE("Scalar usage requires valid ids.");
+ return;
+ }
+
+ ScalarKey uniqueId{static_cast<uint32_t>(aId), false};
+ StaticMutexAutoLock locker(gTelemetryScalarsMutex);
+
+ if (internal_CanRecordScalar(locker, uniqueId, false) != ScalarResult::Ok) {
+ // We can't record this scalar. Bail out.
+ return;
+ }
+
+ // Accumulate in the child process if needed.
+ if (!XRE_IsParentProcess()) {
+ TelemetryIPCAccumulator::RecordChildScalarAction(
+ uniqueId.id, uniqueId.dynamic, ScalarActionType::eAdd,
+ ScalarVariant(aValue));
+ return;
+ }
+
+ if (internal_IsScalarDeserializing(locker)) {
+ internal_RecordScalarAction(locker, uniqueId.id, uniqueId.dynamic,
+ ScalarActionType::eAdd, ScalarVariant(aValue));
+ return;
+ }
+
+ ScalarBase* scalar = nullptr;
+ nsresult rv =
+ internal_GetScalarByEnum(locker, uniqueId, ProcessID::Parent, &scalar);
+ if (NS_FAILED(rv)) {
+ return;
+ }
+
+ scalar->AddValue(aValue);
+}
+
+/**
+ * Adds the value to the given keyed scalar.
+ *
+ * @param aId The scalar enum id.
+ * @param aKey The key name.
+ * @param aVal The numeric value to add to the scalar.
+ */
+void TelemetryScalar::Add(mozilla::Telemetry::ScalarID aId,
+ const nsAString& aKey, uint32_t aValue) {
+ if (NS_WARN_IF(!IsValidEnumId(aId))) {
+ MOZ_ASSERT_UNREACHABLE("Scalar usage requires valid ids.");
+ return;
+ }
+
+ ScalarKey uniqueId{static_cast<uint32_t>(aId), false};
+ StaticMutexAutoLock locker(gTelemetryScalarsMutex);
+
+ if (internal_CanRecordScalar(locker, uniqueId, true) != ScalarResult::Ok) {
+ // We can't record this scalar. Bail out.
+ return;
+ }
+
+ // Accumulate in the child process if needed.
+ if (!XRE_IsParentProcess()) {
+ TelemetryIPCAccumulator::RecordChildKeyedScalarAction(
+ uniqueId.id, uniqueId.dynamic, aKey, ScalarActionType::eAdd,
+ ScalarVariant(aValue));
+ return;
+ }
+
+ if (internal_IsScalarDeserializing(locker)) {
+ internal_RecordKeyedScalarAction(locker, uniqueId.id, uniqueId.dynamic,
+ aKey, ScalarActionType::eAdd,
+ ScalarVariant(aValue));
+ return;
+ }
+
+ KeyedScalar* scalar = nullptr;
+ nsresult rv = internal_GetKeyedScalarByEnum(locker, uniqueId,
+ ProcessID::Parent, &scalar);
+ if (NS_FAILED(rv)) {
+ return;
+ }
+
+ scalar->AddValue(locker, aKey, aValue);
+}
+
+/**
+ * Sets the scalar to the given value.
+ *
+ * @param aName The scalar name.
+ * @param aVal The value to set the scalar to.
+ * @param aCx The JS context.
+ * @return NS_OK (always) so that the JS API call doesn't throw. In case of
+ * errors, a warning level message is printed in the browser console.
+ */
+nsresult TelemetryScalar::Set(const nsACString& aName, JS::HandleValue aVal,
+ JSContext* aCx) {
+ // Unpack the aVal to nsIVariant. This uses the JS context.
+ nsCOMPtr<nsIVariant> unpackedVal;
+ nsresult rv = nsContentUtils::XPConnect()->JSToVariant(
+ aCx, aVal, getter_AddRefs(unpackedVal));
+ if (NS_FAILED(rv)) {
+ internal_LogScalarError(aName, ScalarResult::CannotUnpackVariant);
+ return NS_OK;
+ }
+
+ ScalarResult sr;
+ {
+ StaticMutexAutoLock locker(gTelemetryScalarsMutex);
+ sr = internal_UpdateScalar(locker, aName, ScalarActionType::eSet,
+ unpackedVal);
+ }
+
+ // Warn the user about the error if we need to.
+ if (sr != ScalarResult::Ok) {
+ internal_LogScalarError(aName, sr);
+ }
+
+ return NS_OK;
+}
+
+/**
+ * Sets the keyed scalar to the given value.
+ *
+ * @param aName The scalar name.
+ * @param aKey The key name.
+ * @param aVal The value to set the scalar to.
+ * @param aCx The JS context.
+ * @return NS_OK (always) so that the JS API call doesn't throw. In case of
+ * errors, a warning level message is printed in the browser console.
+ */
+nsresult TelemetryScalar::Set(const nsACString& aName, const nsAString& aKey,
+ JS::HandleValue aVal, JSContext* aCx) {
+ // Unpack the aVal to nsIVariant. This uses the JS context.
+ nsCOMPtr<nsIVariant> unpackedVal;
+ nsresult rv = nsContentUtils::XPConnect()->JSToVariant(
+ aCx, aVal, getter_AddRefs(unpackedVal));
+ if (NS_FAILED(rv)) {
+ internal_LogScalarError(aName, ScalarResult::CannotUnpackVariant);
+ return NS_OK;
+ }
+
+ ScalarResult sr;
+ {
+ StaticMutexAutoLock locker(gTelemetryScalarsMutex);
+ sr = internal_UpdateKeyedScalar(locker, aName, aKey, ScalarActionType::eSet,
+ unpackedVal);
+ }
+
+ // Warn the user about the error if we need to.
+ if (sr != ScalarResult::Ok) {
+ internal_LogScalarError(aName, sr);
+ }
+
+ return NS_OK;
+}
+
+/**
+ * Sets the scalar to the given numeric value.
+ *
+ * @param aId The scalar enum id.
+ * @param aValue The numeric, unsigned value to set the scalar to.
+ */
+void TelemetryScalar::Set(mozilla::Telemetry::ScalarID aId, uint32_t aValue) {
+ if (NS_WARN_IF(!IsValidEnumId(aId))) {
+ MOZ_ASSERT_UNREACHABLE("Scalar usage requires valid ids.");
+ return;
+ }
+
+ ScalarKey uniqueId{static_cast<uint32_t>(aId), false};
+ StaticMutexAutoLock locker(gTelemetryScalarsMutex);
+
+ if (internal_CanRecordScalar(locker, uniqueId, false) != ScalarResult::Ok) {
+ // We can't record this scalar. Bail out.
+ return;
+ }
+
+ // Accumulate in the child process if needed.
+ if (!XRE_IsParentProcess()) {
+ TelemetryIPCAccumulator::RecordChildScalarAction(
+ uniqueId.id, uniqueId.dynamic, ScalarActionType::eSet,
+ ScalarVariant(aValue));
+ return;
+ }
+
+ if (internal_IsScalarDeserializing(locker)) {
+ internal_RecordScalarAction(locker, uniqueId.id, uniqueId.dynamic,
+ ScalarActionType::eSet, ScalarVariant(aValue));
+ return;
+ }
+
+ ScalarBase* scalar = nullptr;
+ nsresult rv =
+ internal_GetScalarByEnum(locker, uniqueId, ProcessID::Parent, &scalar);
+ if (NS_FAILED(rv)) {
+ return;
+ }
+
+ scalar->SetValue(aValue);
+}
+
+/**
+ * Sets the scalar to the given string value.
+ *
+ * @param aId The scalar enum id.
+ * @param aValue The string value to set the scalar to.
+ */
+void TelemetryScalar::Set(mozilla::Telemetry::ScalarID aId,
+ const nsAString& aValue) {
+ if (NS_WARN_IF(!IsValidEnumId(aId))) {
+ MOZ_ASSERT_UNREACHABLE("Scalar usage requires valid ids.");
+ return;
+ }
+
+ ScalarKey uniqueId{static_cast<uint32_t>(aId), false};
+ StaticMutexAutoLock locker(gTelemetryScalarsMutex);
+
+ if (internal_CanRecordScalar(locker, uniqueId, false) != ScalarResult::Ok) {
+ // We can't record this scalar. Bail out.
+ return;
+ }
+
+ // Accumulate in the child process if needed.
+ if (!XRE_IsParentProcess()) {
+ TelemetryIPCAccumulator::RecordChildScalarAction(
+ uniqueId.id, uniqueId.dynamic, ScalarActionType::eSet,
+ ScalarVariant(nsString(aValue)));
+ return;
+ }
+
+ if (internal_IsScalarDeserializing(locker)) {
+ internal_RecordScalarAction(locker, uniqueId.id, uniqueId.dynamic,
+ ScalarActionType::eSet,
+ ScalarVariant(nsString(aValue)));
+ return;
+ }
+
+ ScalarBase* scalar = nullptr;
+ nsresult rv =
+ internal_GetScalarByEnum(locker, uniqueId, ProcessID::Parent, &scalar);
+ if (NS_FAILED(rv)) {
+ return;
+ }
+
+ scalar->SetValue(aValue);
+}
+
+/**
+ * Sets the scalar to the given boolean value.
+ *
+ * @param aId The scalar enum id.
+ * @param aValue The boolean value to set the scalar to.
+ */
+void TelemetryScalar::Set(mozilla::Telemetry::ScalarID aId, bool aValue) {
+ if (NS_WARN_IF(!IsValidEnumId(aId))) {
+ MOZ_ASSERT_UNREACHABLE("Scalar usage requires valid ids.");
+ return;
+ }
+
+ ScalarKey uniqueId{static_cast<uint32_t>(aId), false};
+ StaticMutexAutoLock locker(gTelemetryScalarsMutex);
+
+ if (internal_CanRecordScalar(locker, uniqueId, false) != ScalarResult::Ok) {
+ // We can't record this scalar. Bail out.
+ return;
+ }
+
+ // Accumulate in the child process if needed.
+ if (!XRE_IsParentProcess()) {
+ TelemetryIPCAccumulator::RecordChildScalarAction(
+ uniqueId.id, uniqueId.dynamic, ScalarActionType::eSet,
+ ScalarVariant(aValue));
+ return;
+ }
+
+ if (internal_IsScalarDeserializing(locker)) {
+ internal_RecordScalarAction(locker, uniqueId.id, uniqueId.dynamic,
+ ScalarActionType::eSet, ScalarVariant(aValue));
+ return;
+ }
+
+ ScalarBase* scalar = nullptr;
+ nsresult rv =
+ internal_GetScalarByEnum(locker, uniqueId, ProcessID::Parent, &scalar);
+ if (NS_FAILED(rv)) {
+ return;
+ }
+
+ scalar->SetValue(aValue);
+}
+
+/**
+ * Sets the keyed scalar to the given numeric value.
+ *
+ * @param aId The scalar enum id.
+ * @param aKey The scalar key.
+ * @param aValue The numeric, unsigned value to set the scalar to.
+ */
+void TelemetryScalar::Set(mozilla::Telemetry::ScalarID aId,
+ const nsAString& aKey, uint32_t aValue) {
+ if (NS_WARN_IF(!IsValidEnumId(aId))) {
+ MOZ_ASSERT_UNREACHABLE("Scalar usage requires valid ids.");
+ return;
+ }
+
+ ScalarKey uniqueId{static_cast<uint32_t>(aId), false};
+ StaticMutexAutoLock locker(gTelemetryScalarsMutex);
+
+ if (internal_CanRecordScalar(locker, uniqueId, true) != ScalarResult::Ok) {
+ // We can't record this scalar. Bail out.
+ return;
+ }
+
+ // Accumulate in the child process if needed.
+ if (!XRE_IsParentProcess()) {
+ TelemetryIPCAccumulator::RecordChildKeyedScalarAction(
+ uniqueId.id, uniqueId.dynamic, aKey, ScalarActionType::eSet,
+ ScalarVariant(aValue));
+ return;
+ }
+
+ if (internal_IsScalarDeserializing(locker)) {
+ internal_RecordKeyedScalarAction(locker, uniqueId.id, uniqueId.dynamic,
+ aKey, ScalarActionType::eSet,
+ ScalarVariant(aValue));
+ return;
+ }
+
+ KeyedScalar* scalar = nullptr;
+ nsresult rv = internal_GetKeyedScalarByEnum(locker, uniqueId,
+ ProcessID::Parent, &scalar);
+ if (NS_FAILED(rv)) {
+ return;
+ }
+
+ scalar->SetValue(locker, aKey, aValue);
+}
+
+/**
+ * Sets the scalar to the given boolean value.
+ *
+ * @param aId The scalar enum id.
+ * @param aKey The scalar key.
+ * @param aValue The boolean value to set the scalar to.
+ */
+void TelemetryScalar::Set(mozilla::Telemetry::ScalarID aId,
+ const nsAString& aKey, bool aValue) {
+ if (NS_WARN_IF(!IsValidEnumId(aId))) {
+ MOZ_ASSERT_UNREACHABLE("Scalar usage requires valid ids.");
+ return;
+ }
+
+ ScalarKey uniqueId{static_cast<uint32_t>(aId), false};
+ StaticMutexAutoLock locker(gTelemetryScalarsMutex);
+
+ if (internal_CanRecordScalar(locker, uniqueId, true) != ScalarResult::Ok) {
+ // We can't record this scalar. Bail out.
+ return;
+ }
+
+ // Accumulate in the child process if needed.
+ if (!XRE_IsParentProcess()) {
+ TelemetryIPCAccumulator::RecordChildKeyedScalarAction(
+ uniqueId.id, uniqueId.dynamic, aKey, ScalarActionType::eSet,
+ ScalarVariant(aValue));
+ return;
+ }
+
+ if (internal_IsScalarDeserializing(locker)) {
+ internal_RecordKeyedScalarAction(locker, uniqueId.id, uniqueId.dynamic,
+ aKey, ScalarActionType::eSet,
+ ScalarVariant(aValue));
+ return;
+ }
+
+ KeyedScalar* scalar = nullptr;
+ nsresult rv = internal_GetKeyedScalarByEnum(locker, uniqueId,
+ ProcessID::Parent, &scalar);
+ if (NS_FAILED(rv)) {
+ return;
+ }
+
+ scalar->SetValue(locker, aKey, aValue);
+}
+
+/**
+ * Sets the scalar to the maximum of the current and the passed value.
+ *
+ * @param aName The scalar name.
+ * @param aVal The numeric value to set the scalar to.
+ * @param aCx The JS context.
+ * @return NS_OK (always) so that the JS API call doesn't throw. In case of
+ * errors, a warning level message is printed in the browser console.
+ */
+nsresult TelemetryScalar::SetMaximum(const nsACString& aName,
+ JS::HandleValue aVal, JSContext* aCx) {
+ // Unpack the aVal to nsIVariant. This uses the JS context.
+ nsCOMPtr<nsIVariant> unpackedVal;
+ nsresult rv = nsContentUtils::XPConnect()->JSToVariant(
+ aCx, aVal, getter_AddRefs(unpackedVal));
+ if (NS_FAILED(rv)) {
+ internal_LogScalarError(aName, ScalarResult::CannotUnpackVariant);
+ return NS_OK;
+ }
+
+ ScalarResult sr;
+ {
+ StaticMutexAutoLock locker(gTelemetryScalarsMutex);
+ sr = internal_UpdateScalar(locker, aName, ScalarActionType::eSetMaximum,
+ unpackedVal);
+ }
+
+ // Warn the user about the error if we need to.
+ if (sr != ScalarResult::Ok) {
+ internal_LogScalarError(aName, sr);
+ }
+
+ return NS_OK;
+}
+
+/**
+ * Sets the scalar to the maximum of the current and the passed value.
+ *
+ * @param aName The scalar name.
+ * @param aKey The key name.
+ * @param aVal The numeric value to set the scalar to.
+ * @param aCx The JS context.
+ * @return NS_OK (always) so that the JS API call doesn't throw. In case of
+ * errors, a warning level message is printed in the browser console.
+ */
+nsresult TelemetryScalar::SetMaximum(const nsACString& aName,
+ const nsAString& aKey,
+ JS::HandleValue aVal, JSContext* aCx) {
+ // Unpack the aVal to nsIVariant. This uses the JS context.
+ nsCOMPtr<nsIVariant> unpackedVal;
+ nsresult rv = nsContentUtils::XPConnect()->JSToVariant(
+ aCx, aVal, getter_AddRefs(unpackedVal));
+ if (NS_FAILED(rv)) {
+ internal_LogScalarError(aName, ScalarResult::CannotUnpackVariant);
+ return NS_OK;
+ }
+
+ ScalarResult sr;
+ {
+ StaticMutexAutoLock locker(gTelemetryScalarsMutex);
+ sr = internal_UpdateKeyedScalar(locker, aName, aKey,
+ ScalarActionType::eSetMaximum, unpackedVal);
+ }
+
+ // Warn the user about the error if we need to.
+ if (sr != ScalarResult::Ok) {
+ internal_LogScalarError(aName, sr);
+ }
+
+ return NS_OK;
+}
+
+/**
+ * Sets the scalar to the maximum of the current and the passed value.
+ *
+ * @param aId The scalar enum id.
+ * @param aValue The numeric value to set the scalar to.
+ */
+void TelemetryScalar::SetMaximum(mozilla::Telemetry::ScalarID aId,
+ uint32_t aValue) {
+ if (NS_WARN_IF(!IsValidEnumId(aId))) {
+ MOZ_ASSERT_UNREACHABLE("Scalar usage requires valid ids.");
+ return;
+ }
+
+ ScalarKey uniqueId{static_cast<uint32_t>(aId), false};
+ StaticMutexAutoLock locker(gTelemetryScalarsMutex);
+
+ if (internal_CanRecordScalar(locker, uniqueId, false) != ScalarResult::Ok) {
+ // We can't record this scalar. Bail out.
+ return;
+ }
+
+ // Accumulate in the child process if needed.
+ if (!XRE_IsParentProcess()) {
+ TelemetryIPCAccumulator::RecordChildScalarAction(
+ uniqueId.id, uniqueId.dynamic, ScalarActionType::eSetMaximum,
+ ScalarVariant(aValue));
+ return;
+ }
+
+ if (internal_IsScalarDeserializing(locker)) {
+ internal_RecordScalarAction(locker, uniqueId.id, uniqueId.dynamic,
+ ScalarActionType::eSetMaximum,
+ ScalarVariant(aValue));
+ return;
+ }
+
+ ScalarBase* scalar = nullptr;
+ nsresult rv =
+ internal_GetScalarByEnum(locker, uniqueId, ProcessID::Parent, &scalar);
+ if (NS_FAILED(rv)) {
+ return;
+ }
+
+ scalar->SetMaximum(aValue);
+}
+
+/**
+ * Sets the keyed scalar to the maximum of the current and the passed value.
+ *
+ * @param aId The scalar enum id.
+ * @param aKey The key name.
+ * @param aValue The numeric value to set the scalar to.
+ */
+void TelemetryScalar::SetMaximum(mozilla::Telemetry::ScalarID aId,
+ const nsAString& aKey, uint32_t aValue) {
+ if (NS_WARN_IF(!IsValidEnumId(aId))) {
+ MOZ_ASSERT_UNREACHABLE("Scalar usage requires valid ids.");
+ return;
+ }
+
+ ScalarKey uniqueId{static_cast<uint32_t>(aId), false};
+ StaticMutexAutoLock locker(gTelemetryScalarsMutex);
+
+ if (internal_CanRecordScalar(locker, uniqueId, true) != ScalarResult::Ok) {
+ // We can't record this scalar. Bail out.
+ return;
+ }
+
+ // Accumulate in the child process if needed.
+ if (!XRE_IsParentProcess()) {
+ TelemetryIPCAccumulator::RecordChildKeyedScalarAction(
+ uniqueId.id, uniqueId.dynamic, aKey, ScalarActionType::eSetMaximum,
+ ScalarVariant(aValue));
+ return;
+ }
+
+ if (internal_IsScalarDeserializing(locker)) {
+ internal_RecordKeyedScalarAction(locker, uniqueId.id, uniqueId.dynamic,
+ aKey, ScalarActionType::eSetMaximum,
+ ScalarVariant(aValue));
+ return;
+ }
+
+ KeyedScalar* scalar = nullptr;
+ nsresult rv = internal_GetKeyedScalarByEnum(locker, uniqueId,
+ ProcessID::Parent, &scalar);
+ if (NS_FAILED(rv)) {
+ return;
+ }
+
+ scalar->SetMaximum(locker, aKey, aValue);
+}
+
+nsresult TelemetryScalar::CreateSnapshots(unsigned int aDataset,
+ bool aClearScalars, JSContext* aCx,
+ uint8_t optional_argc,
+ JS::MutableHandle<JS::Value> aResult,
+ bool aFilterTest,
+ const nsACString& aStoreName) {
+ MOZ_ASSERT(
+ XRE_IsParentProcess(),
+ "Snapshotting scalars should only happen in the parent processes.");
+ // If no arguments were passed in, apply the default value.
+ if (!optional_argc) {
+ aClearScalars = false;
+ }
+
+ JS::Rooted<JSObject*> root_obj(aCx, JS_NewPlainObject(aCx));
+ if (!root_obj) {
+ return NS_ERROR_FAILURE;
+ }
+ aResult.setObject(*root_obj);
+
+ // Return `{}` in child processes.
+ if (!XRE_IsParentProcess()) {
+ return NS_OK;
+ }
+
+ // Only lock the mutex while accessing our data, without locking any JS
+ // related code.
+ ScalarSnapshotTable scalarsToReflect;
+ {
+ StaticMutexAutoLock locker(gTelemetryScalarsMutex);
+
+ nsresult rv = internal_GetScalarSnapshot(locker, scalarsToReflect, aDataset,
+ aClearScalars, aStoreName);
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+ }
+
+ // Reflect it to JS.
+ for (auto iter = scalarsToReflect.Iter(); !iter.Done(); iter.Next()) {
+ ScalarTupleArray& processScalars = iter.Data();
+ const char* processName = GetNameForProcessID(ProcessID(iter.Key()));
+
+ // Create the object that will hold the scalars for this process and add it
+ // to the returned root object.
+ JS::RootedObject processObj(aCx, JS_NewPlainObject(aCx));
+ if (!processObj || !JS_DefineProperty(aCx, root_obj, processName,
+ processObj, JSPROP_ENUMERATE)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ for (ScalarTupleArray::size_type i = 0; i < processScalars.Length(); i++) {
+ const ScalarDataTuple& scalar = processScalars[i];
+
+ const char* scalarName = mozilla::Get<0>(scalar);
+ if (aFilterTest && strncmp(TEST_SCALAR_PREFIX, scalarName,
+ strlen(TEST_SCALAR_PREFIX)) == 0) {
+ continue;
+ }
+
+ // Convert it to a JS Val.
+ JS::Rooted<JS::Value> scalarJsValue(aCx);
+ nsresult rv = nsContentUtils::XPConnect()->VariantToJS(
+ aCx, processObj, mozilla::Get<1>(scalar), &scalarJsValue);
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+
+ // Add it to the scalar object.
+ if (!JS_DefineProperty(aCx, processObj, scalarName, scalarJsValue,
+ JSPROP_ENUMERATE)) {
+ return NS_ERROR_FAILURE;
+ }
+ }
+ }
+
+ return NS_OK;
+}
+
+nsresult TelemetryScalar::CreateKeyedSnapshots(
+ unsigned int aDataset, bool aClearScalars, JSContext* aCx,
+ uint8_t optional_argc, JS::MutableHandle<JS::Value> aResult,
+ bool aFilterTest, const nsACString& aStoreName) {
+ MOZ_ASSERT(
+ XRE_IsParentProcess(),
+ "Snapshotting scalars should only happen in the parent processes.");
+ // If no arguments were passed in, apply the default value.
+ if (!optional_argc) {
+ aClearScalars = false;
+ }
+
+ JS::Rooted<JSObject*> root_obj(aCx, JS_NewPlainObject(aCx));
+ if (!root_obj) {
+ return NS_ERROR_FAILURE;
+ }
+ aResult.setObject(*root_obj);
+
+ // Return `{}` in child processes.
+ if (!XRE_IsParentProcess()) {
+ return NS_OK;
+ }
+
+ // Only lock the mutex while accessing our data, without locking any JS
+ // related code.
+ KeyedScalarSnapshotTable scalarsToReflect;
+ {
+ StaticMutexAutoLock locker(gTelemetryScalarsMutex);
+
+ nsresult rv = internal_GetKeyedScalarSnapshot(
+ locker, scalarsToReflect, aDataset, aClearScalars, aStoreName);
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+ }
+
+ // Reflect it to JS.
+ for (auto iter = scalarsToReflect.Iter(); !iter.Done(); iter.Next()) {
+ KeyedScalarTupleArray& processScalars = iter.Data();
+ const char* processName = GetNameForProcessID(ProcessID(iter.Key()));
+
+ // Create the object that will hold the scalars for this process and add it
+ // to the returned root object.
+ JS::RootedObject processObj(aCx, JS_NewPlainObject(aCx));
+ if (!processObj || !JS_DefineProperty(aCx, root_obj, processName,
+ processObj, JSPROP_ENUMERATE)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ for (KeyedScalarTupleArray::size_type i = 0; i < processScalars.Length();
+ i++) {
+ const KeyedScalarDataTuple& keyedScalarData = processScalars[i];
+
+ const char* scalarName = mozilla::Get<0>(keyedScalarData);
+ if (aFilterTest && strncmp(TEST_SCALAR_PREFIX, scalarName,
+ strlen(TEST_SCALAR_PREFIX)) == 0) {
+ continue;
+ }
+
+ // Go through each keyed scalar and create a keyed scalar object.
+ // This object will hold the values for all the keyed scalar keys.
+ JS::RootedObject keyedScalarObj(aCx, JS_NewPlainObject(aCx));
+
+ // Define a property for each scalar key, then add it to the keyed scalar
+ // object.
+ const nsTArray<KeyedScalar::KeyValuePair>& keyProps =
+ mozilla::Get<1>(keyedScalarData);
+ for (uint32_t i = 0; i < keyProps.Length(); i++) {
+ const KeyedScalar::KeyValuePair& keyData = keyProps[i];
+
+ // Convert the value for the key to a JSValue.
+ JS::Rooted<JS::Value> keyJsValue(aCx);
+ nsresult rv = nsContentUtils::XPConnect()->VariantToJS(
+ aCx, keyedScalarObj, keyData.second, &keyJsValue);
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+
+ // Add the key to the scalar representation.
+ const NS_ConvertUTF8toUTF16 key(keyData.first);
+ if (!JS_DefineUCProperty(aCx, keyedScalarObj, key.Data(), key.Length(),
+ keyJsValue, JSPROP_ENUMERATE)) {
+ return NS_ERROR_FAILURE;
+ }
+ }
+
+ // Add the scalar to the root object.
+ if (!JS_DefineProperty(aCx, processObj, scalarName, keyedScalarObj,
+ JSPROP_ENUMERATE)) {
+ return NS_ERROR_FAILURE;
+ }
+ }
+ }
+
+ return NS_OK;
+}
+
+nsresult TelemetryScalar::RegisterScalars(const nsACString& aCategoryName,
+ JS::Handle<JS::Value> aScalarData,
+ bool aBuiltin, JSContext* cx) {
+ MOZ_ASSERT(XRE_IsParentProcess(),
+ "Dynamic scalars should only be created in the parent process.");
+
+ if (!IsValidIdentifierString(aCategoryName, kMaximumCategoryNameLength, true,
+ false)) {
+ JS_ReportErrorASCII(cx, "Invalid category name %s.",
+ PromiseFlatCString(aCategoryName).get());
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ if (!aScalarData.isObject()) {
+ JS_ReportErrorASCII(cx, "Scalar data parameter should be an object");
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ JS::RootedObject obj(cx, &aScalarData.toObject());
+ JS::Rooted<JS::IdVector> scalarPropertyIds(cx, JS::IdVector(cx));
+ if (!JS_Enumerate(cx, obj, &scalarPropertyIds)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ // Collect the scalar data into local storage first.
+ // Only after successfully validating all contained scalars will we register
+ // them into global storage.
+ nsTArray<DynamicScalarInfo> newScalarInfos;
+
+ for (size_t i = 0, n = scalarPropertyIds.length(); i < n; i++) {
+ nsAutoJSString scalarName;
+ if (!scalarName.init(cx, scalarPropertyIds[i])) {
+ return NS_ERROR_FAILURE;
+ }
+
+ if (!IsValidIdentifierString(NS_ConvertUTF16toUTF8(scalarName),
+ kMaximumScalarNameLength, false, true)) {
+ JS_ReportErrorASCII(
+ cx, "Invalid scalar name %s.",
+ PromiseFlatCString(NS_ConvertUTF16toUTF8(scalarName)).get());
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ // Join the category and the probe names.
+ nsPrintfCString fullName("%s.%s", PromiseFlatCString(aCategoryName).get(),
+ NS_ConvertUTF16toUTF8(scalarName).get());
+
+ JS::RootedValue value(cx);
+ if (!JS_GetPropertyById(cx, obj, scalarPropertyIds[i], &value) ||
+ !value.isObject()) {
+ return NS_ERROR_FAILURE;
+ }
+ JS::RootedObject scalarDef(cx, &value.toObject());
+
+ // Get the scalar's kind.
+ if (!JS_GetProperty(cx, scalarDef, "kind", &value) || !value.isInt32()) {
+ JS_ReportErrorASCII(cx, "Invalid or missing 'kind' for scalar %s.",
+ PromiseFlatCString(fullName).get());
+ return NS_ERROR_FAILURE;
+ }
+ uint32_t kind = static_cast<uint32_t>(value.toInt32());
+
+ // Get the optional scalar's recording policy (default to false).
+ bool hasProperty = false;
+ bool recordOnRelease = false;
+ if (JS_HasProperty(cx, scalarDef, "record_on_release", &hasProperty) &&
+ hasProperty) {
+ if (!JS_GetProperty(cx, scalarDef, "record_on_release", &value) ||
+ !value.isBoolean()) {
+ JS_ReportErrorASCII(cx, "Invalid 'record_on_release' for scalar %s.",
+ PromiseFlatCString(fullName).get());
+ return NS_ERROR_FAILURE;
+ }
+ recordOnRelease = static_cast<bool>(value.toBoolean());
+ }
+
+ // Get the optional scalar's keyed (default to false).
+ bool keyed = false;
+ if (JS_HasProperty(cx, scalarDef, "keyed", &hasProperty) && hasProperty) {
+ if (!JS_GetProperty(cx, scalarDef, "keyed", &value) ||
+ !value.isBoolean()) {
+ JS_ReportErrorASCII(cx, "Invalid 'keyed' for scalar %s.",
+ PromiseFlatCString(fullName).get());
+ return NS_ERROR_FAILURE;
+ }
+ keyed = static_cast<bool>(value.toBoolean());
+ }
+
+ // Get the optional scalar's expired state (default to false).
+ bool expired = false;
+ if (JS_HasProperty(cx, scalarDef, "expired", &hasProperty) && hasProperty) {
+ if (!JS_GetProperty(cx, scalarDef, "expired", &value) ||
+ !value.isBoolean()) {
+ JS_ReportErrorASCII(cx, "Invalid 'expired' for scalar %s.",
+ PromiseFlatCString(fullName).get());
+ return NS_ERROR_FAILURE;
+ }
+ expired = static_cast<bool>(value.toBoolean());
+ }
+
+ // Get the scalar's optional stores list (default to ["main"]).
+ nsTArray<nsCString> stores;
+ if (JS_HasProperty(cx, scalarDef, "stores", &hasProperty) && hasProperty) {
+ bool isArray = false;
+ if (!JS_GetProperty(cx, scalarDef, "stores", &value) ||
+ !JS::IsArrayObject(cx, value, &isArray) || !isArray) {
+ JS_ReportErrorASCII(cx, "Invalid 'stores' for scalar %s.",
+ PromiseFlatCString(fullName).get());
+ return NS_ERROR_FAILURE;
+ }
+
+ JS::RootedObject arrayObj(cx, &value.toObject());
+ uint32_t storesLength = 0;
+ if (!JS::GetArrayLength(cx, arrayObj, &storesLength)) {
+ JS_ReportErrorASCII(cx,
+ "Can't get 'stores' array length for scalar %s.",
+ PromiseFlatCString(fullName).get());
+ return NS_ERROR_FAILURE;
+ }
+
+ for (uint32_t i = 0; i < storesLength; ++i) {
+ JS::Rooted<JS::Value> elt(cx);
+ if (!JS_GetElement(cx, arrayObj, i, &elt)) {
+ JS_ReportErrorASCII(
+ cx, "Can't get element from scalar %s 'stores' array.",
+ PromiseFlatCString(fullName).get());
+ return NS_ERROR_FAILURE;
+ }
+ if (!elt.isString()) {
+ JS_ReportErrorASCII(cx,
+ "Element in scalar %s 'stores' array isn't a "
+ "string.",
+ PromiseFlatCString(fullName).get());
+ return NS_ERROR_FAILURE;
+ }
+
+ nsAutoJSString jsStr;
+ if (!jsStr.init(cx, elt)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ stores.AppendElement(NS_ConvertUTF16toUTF8(jsStr));
+ }
+ // In the event of the usual case (just "main"), save the storage.
+ if (stores.Length() == 1 && stores[0].EqualsLiteral("main")) {
+ stores.TruncateLength(0);
+ }
+ }
+
+ // We defer the actual registration here in case any other event description
+ // is invalid. In that case we don't need to roll back any partial
+ // registration.
+ newScalarInfos.AppendElement(
+ DynamicScalarInfo{kind, recordOnRelease, expired, fullName, keyed,
+ aBuiltin, std::move(stores)});
+ }
+
+ // Register the dynamic definition on the parent process.
+ nsTArray<DynamicScalarDefinition> ipcDefinitions;
+ {
+ StaticMutexAutoLock locker(gTelemetryScalarsMutex);
+ ::internal_RegisterScalars(locker, newScalarInfos);
+
+ // Convert the internal scalar representation to a stripped down IPC one.
+ ::internal_DynamicScalarToIPC(locker, newScalarInfos, ipcDefinitions);
+ }
+
+ // Propagate the registration to all the content-processes.
+ // Do not hold the mutex while calling IPC.
+ ::internal_BroadcastDefinitions(ipcDefinitions);
+
+ return NS_OK;
+}
+
+/**
+ * Count in Scalars how many of which events were recorded. See bug 1440673
+ *
+ * Event Telemetry unfortunately cannot use vanilla ScalarAdd because it needs
+ * to summarize events recorded in different processes to the
+ * telemetry.event_counts of the same process. Including "dynamic".
+ *
+ * @param aUniqueEventName - expected to be category#object#method
+ * @param aProcessType - the process of the event being summarized
+ * @param aDynamic - whether the event being summarized was dynamic
+ */
+void TelemetryScalar::SummarizeEvent(const nsCString& aUniqueEventName,
+ ProcessID aProcessType, bool aDynamic) {
+ MOZ_ASSERT(XRE_IsParentProcess(),
+ "Only summarize events in the parent process");
+ if (!XRE_IsParentProcess()) {
+ return;
+ }
+
+ StaticMutexAutoLock lock(gTelemetryScalarsMutex);
+
+ ScalarKey scalarKey{static_cast<uint32_t>(ScalarID::TELEMETRY_EVENT_COUNTS),
+ aDynamic};
+ if (aDynamic) {
+ nsresult rv = internal_GetEnumByScalarName(
+ lock, nsAutoCString("telemetry.dynamic_event_counts"), &scalarKey);
+ if (NS_FAILED(rv)) {
+ NS_WARNING(
+ "NS_FAILED getting ScalarKey for telemetry.dynamic_event_counts");
+ return;
+ }
+ }
+
+ KeyedScalar* scalar = nullptr;
+ nsresult rv =
+ internal_GetKeyedScalarByEnum(lock, scalarKey, aProcessType, &scalar);
+
+ if (NS_FAILED(rv)) {
+ NS_WARNING("NS_FAILED getting keyed scalar for event summary. Wut.");
+ return;
+ }
+
+ // Set this each time as it may have been cleared and recreated between calls
+ scalar->SetMaximumNumberOfKeys(kMaxEventSummaryKeys);
+
+ scalar->AddValue(lock, NS_ConvertASCIItoUTF16(aUniqueEventName), 1);
+}
+
+/**
+ * Resets all the stored scalars. This is intended to be only used in tests.
+ */
+void TelemetryScalar::ClearScalars() {
+ MOZ_ASSERT(XRE_IsParentProcess(),
+ "Scalars should only be cleared in the parent process.");
+ if (!XRE_IsParentProcess()) {
+ return;
+ }
+
+ StaticMutexAutoLock locker(gTelemetryScalarsMutex);
+ gScalarStorageMap.Clear();
+ gKeyedScalarStorageMap.Clear();
+ gDynamicBuiltinScalarStorageMap.Clear();
+ gDynamicBuiltinKeyedScalarStorageMap.Clear();
+ gScalarsActions = nullptr;
+ gKeyedScalarsActions = nullptr;
+}
+
+size_t TelemetryScalar::GetMapShallowSizesOfExcludingThis(
+ mozilla::MallocSizeOf aMallocSizeOf) {
+ StaticMutexAutoLock locker(gTelemetryScalarsMutex);
+ return gScalarNameIDMap.ShallowSizeOfExcludingThis(aMallocSizeOf);
+}
+
+size_t TelemetryScalar::GetScalarSizesOfIncludingThis(
+ mozilla::MallocSizeOf aMallocSizeOf) {
+ StaticMutexAutoLock locker(gTelemetryScalarsMutex);
+ size_t n = 0;
+
+ auto getSizeOf = [aMallocSizeOf](auto& storageMap) {
+ size_t partial = 0;
+ for (auto iter = storageMap.Iter(); !iter.Done(); iter.Next()) {
+ auto scalarStorage = iter.UserData();
+ for (auto childIter = scalarStorage->Iter(); !childIter.Done();
+ childIter.Next()) {
+ auto scalar = childIter.UserData();
+ partial += scalar->SizeOfIncludingThis(aMallocSizeOf);
+ }
+ }
+ return partial;
+ };
+
+ // Account for all the storage used for the different scalar types.
+ n += getSizeOf(gScalarStorageMap);
+ n += getSizeOf(gKeyedScalarStorageMap);
+ n += getSizeOf(gDynamicBuiltinScalarStorageMap);
+ n += getSizeOf(gDynamicBuiltinKeyedScalarStorageMap);
+
+ return n;
+}
+
+void TelemetryScalar::UpdateChildData(
+ ProcessID aProcessType,
+ const nsTArray<mozilla::Telemetry::ScalarAction>& aScalarActions) {
+ MOZ_ASSERT(XRE_IsParentProcess(),
+ "The stored child processes scalar data must be updated from the "
+ "parent process.");
+ StaticMutexAutoLock locker(gTelemetryScalarsMutex);
+
+ // If scalars are still being deserialized, we need to record the incoming
+ // operations as well.
+ if (internal_IsScalarDeserializing(locker)) {
+ for (const ScalarAction& action : aScalarActions) {
+ // We're only getting immutable access, so let's copy it
+ ScalarAction copy = action;
+ // Fix up the process type
+ copy.mProcessType = aProcessType;
+ internal_RecordScalarAction(locker, copy);
+ }
+
+ return;
+ }
+
+ internal_ApplyScalarActions(locker, aScalarActions, Some(aProcessType));
+}
+
+void TelemetryScalar::UpdateChildKeyedData(
+ ProcessID aProcessType,
+ const nsTArray<mozilla::Telemetry::KeyedScalarAction>& aScalarActions) {
+ MOZ_ASSERT(XRE_IsParentProcess(),
+ "The stored child processes keyed scalar data must be updated "
+ "from the parent process.");
+ StaticMutexAutoLock locker(gTelemetryScalarsMutex);
+
+ // If scalars are still being deserialized, we need to record the incoming
+ // operations as well.
+ if (internal_IsScalarDeserializing(locker)) {
+ for (const KeyedScalarAction& action : aScalarActions) {
+ // We're only getting immutable access, so let's copy it
+ KeyedScalarAction copy = action;
+ // Fix up the process type
+ copy.mProcessType = aProcessType;
+ internal_RecordKeyedScalarAction(locker, copy);
+ }
+
+ return;
+ }
+
+ internal_ApplyKeyedScalarActions(locker, aScalarActions, Some(aProcessType));
+}
+
+void TelemetryScalar::RecordDiscardedData(
+ ProcessID aProcessType,
+ const mozilla::Telemetry::DiscardedData& aDiscardedData) {
+ MOZ_ASSERT(XRE_IsParentProcess(),
+ "Discarded Data must be updated from the parent process.");
+ StaticMutexAutoLock locker(gTelemetryScalarsMutex);
+ if (!internal_CanRecordBase(locker)) {
+ return;
+ }
+
+ if (GetCurrentProduct() == SupportedProduct::GeckoviewStreaming) {
+ return;
+ }
+
+ ScalarBase* scalar = nullptr;
+ mozilla::DebugOnly<nsresult> rv;
+
+ rv = internal_GetScalarByEnum(
+ locker,
+ ScalarKey{
+ static_cast<uint32_t>(ScalarID::TELEMETRY_DISCARDED_ACCUMULATIONS),
+ false},
+ aProcessType, &scalar);
+ MOZ_ASSERT(NS_SUCCEEDED(rv));
+ scalar->AddValue(aDiscardedData.mDiscardedHistogramAccumulations);
+
+ rv = internal_GetScalarByEnum(
+ locker,
+ ScalarKey{static_cast<uint32_t>(
+ ScalarID::TELEMETRY_DISCARDED_KEYED_ACCUMULATIONS),
+ false},
+ aProcessType, &scalar);
+ MOZ_ASSERT(NS_SUCCEEDED(rv));
+ scalar->AddValue(aDiscardedData.mDiscardedKeyedHistogramAccumulations);
+
+ rv = internal_GetScalarByEnum(
+ locker,
+ ScalarKey{
+ static_cast<uint32_t>(ScalarID::TELEMETRY_DISCARDED_SCALAR_ACTIONS),
+ false},
+ aProcessType, &scalar);
+ MOZ_ASSERT(NS_SUCCEEDED(rv));
+ scalar->AddValue(aDiscardedData.mDiscardedScalarActions);
+
+ rv = internal_GetScalarByEnum(
+ locker,
+ ScalarKey{static_cast<uint32_t>(
+ ScalarID::TELEMETRY_DISCARDED_KEYED_SCALAR_ACTIONS),
+ false},
+ aProcessType, &scalar);
+ MOZ_ASSERT(NS_SUCCEEDED(rv));
+ scalar->AddValue(aDiscardedData.mDiscardedKeyedScalarActions);
+
+ rv = internal_GetScalarByEnum(
+ locker,
+ ScalarKey{
+ static_cast<uint32_t>(ScalarID::TELEMETRY_DISCARDED_CHILD_EVENTS),
+ false},
+ aProcessType, &scalar);
+ MOZ_ASSERT(NS_SUCCEEDED(rv));
+ scalar->AddValue(aDiscardedData.mDiscardedChildEvents);
+}
+
+/**
+ * Get the dynamic scalar definitions in an IPC-friendly
+ * structure.
+ */
+void TelemetryScalar::GetDynamicScalarDefinitions(
+ nsTArray<DynamicScalarDefinition>& aDefArray) {
+ MOZ_ASSERT(XRE_IsParentProcess());
+ if (!gDynamicScalarInfo) {
+ // Don't have dynamic scalar definitions. Bail out!
+ return;
+ }
+
+ StaticMutexAutoLock locker(gTelemetryScalarsMutex);
+ internal_DynamicScalarToIPC(locker, *gDynamicScalarInfo, aDefArray);
+}
+
+/**
+ * This adds the dynamic scalar definitions coming from
+ * the parent process to this child process. If a dynamic
+ * scalar definition is already defined, check if the new definition
+ * makes the scalar expired and eventually update the expiration
+ * state.
+ */
+void TelemetryScalar::AddDynamicScalarDefinitions(
+ const nsTArray<DynamicScalarDefinition>& aDefs) {
+ MOZ_ASSERT(!XRE_IsParentProcess());
+
+ nsTArray<DynamicScalarInfo> dynamicStubs;
+
+ // Populate the definitions array before acquiring the lock.
+ for (auto& def : aDefs) {
+ bool recordOnRelease = def.dataset == nsITelemetry::DATASET_ALL_CHANNELS;
+ dynamicStubs.AppendElement(DynamicScalarInfo{def.type,
+ recordOnRelease,
+ def.expired,
+ def.name,
+ def.keyed,
+ def.builtin,
+ {} /* stores */});
+ }
+
+ {
+ StaticMutexAutoLock locker(gTelemetryScalarsMutex);
+ internal_RegisterScalars(locker, dynamicStubs);
+ }
+}
+
+nsresult TelemetryScalar::GetAllStores(StringHashSet& set) {
+ // Static stores
+ for (uint32_t storeIdx : gScalarStoresTable) {
+ const char* name = &gScalarsStringTable[storeIdx];
+ nsAutoCString store;
+ store.AssignASCII(name);
+ if (!set.PutEntry(store)) {
+ return NS_ERROR_FAILURE;
+ }
+ }
+
+ // Dynamic stores
+ for (auto& ptr : *gDynamicStoreNames) {
+ nsAutoCString store;
+ ptr->ToUTF8String(store);
+ if (!set.PutEntry(store)) {
+ return NS_ERROR_FAILURE;
+ }
+ }
+
+ return NS_OK;
+}
+
+////////////////////////////////////////////////////////////////////////
+////////////////////////////////////////////////////////////////////////
+//
+// PUBLIC: GeckoView serialization/deserialization functions.
+
+/**
+ * Write the scalar data to the provided Json object, for
+ * GeckoView measurement persistence. The output format is the same one used
+ * for snapshotting the scalars.
+ *
+ * @param {aWriter} The JSON object to write to.
+ * @returns NS_OK or a failure value explaining why persistence failed.
+ */
+nsresult TelemetryScalar::SerializeScalars(mozilla::JSONWriter& aWriter) {
+ // Get a copy of the data, without clearing.
+ ScalarSnapshotTable scalarsToReflect;
+ {
+ StaticMutexAutoLock locker(gTelemetryScalarsMutex);
+ // For persistence, we care about all the datasets. Worst case, they
+ // will be empty.
+ nsresult rv = internal_GetScalarSnapshot(
+ locker, scalarsToReflect, nsITelemetry::DATASET_PRERELEASE_CHANNELS,
+ false, /*aClearScalars*/
+ "main"_ns);
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+ }
+
+ // Persist the scalars to the JSON object.
+ for (auto iter = scalarsToReflect.Iter(); !iter.Done(); iter.Next()) {
+ ScalarTupleArray& processScalars = iter.Data();
+ const char* processName = GetNameForProcessID(ProcessID(iter.Key()));
+
+ aWriter.StartObjectProperty(mozilla::MakeStringSpan(processName));
+
+ for (const ScalarDataTuple& scalar : processScalars) {
+ nsresult rv = WriteVariantToJSONWriter(
+ mozilla::Get<2>(scalar) /*aScalarType*/,
+ mozilla::Get<1>(scalar) /*aInputValue*/,
+ mozilla::MakeStringSpan(mozilla::Get<0>(scalar)) /*aPropertyName*/,
+ aWriter /*aWriter*/);
+ if (NS_FAILED(rv)) {
+ // Skip this scalar if we failed to write it. We don't bail out just
+ // yet as we may salvage other scalars. We eventually need to call
+ // EndObject.
+ continue;
+ }
+ }
+
+ aWriter.EndObject();
+ }
+
+ return NS_OK;
+}
+
+/**
+ * Write the keyed scalar data to the provided Json object, for
+ * GeckoView measurement persistence. The output format is the same
+ * one used for snapshotting the keyed scalars.
+ *
+ * @param {aWriter} The JSON object to write to.
+ * @returns NS_OK or a failure value explaining why persistence failed.
+ */
+nsresult TelemetryScalar::SerializeKeyedScalars(mozilla::JSONWriter& aWriter) {
+ // Get a copy of the data, without clearing.
+ KeyedScalarSnapshotTable keyedScalarsToReflect;
+ {
+ StaticMutexAutoLock locker(gTelemetryScalarsMutex);
+ // For persistence, we care about all the datasets. Worst case, they
+ // will be empty.
+ nsresult rv = internal_GetKeyedScalarSnapshot(
+ locker, keyedScalarsToReflect,
+ nsITelemetry::DATASET_PRERELEASE_CHANNELS, false, /*aClearScalars*/
+ "main"_ns);
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+ }
+
+ // Persist the scalars to the JSON object.
+ for (auto iter = keyedScalarsToReflect.Iter(); !iter.Done(); iter.Next()) {
+ KeyedScalarTupleArray& processScalars = iter.Data();
+ const char* processName = GetNameForProcessID(ProcessID(iter.Key()));
+
+ aWriter.StartObjectProperty(mozilla::MakeStringSpan(processName));
+
+ for (const KeyedScalarDataTuple& keyedScalarData : processScalars) {
+ aWriter.StartObjectProperty(
+ mozilla::MakeStringSpan(mozilla::Get<0>(keyedScalarData)));
+
+ // Define a property for each scalar key, then add it to the keyed scalar
+ // object.
+ const nsTArray<KeyedScalar::KeyValuePair>& keyProps =
+ mozilla::Get<1>(keyedScalarData);
+ for (const KeyedScalar::KeyValuePair& keyData : keyProps) {
+ nsresult rv = WriteVariantToJSONWriter(
+ mozilla::Get<2>(keyedScalarData) /*aScalarType*/,
+ keyData.second /*aInputValue*/,
+ PromiseFlatCString(keyData.first) /*aOutKey*/, aWriter /*aWriter*/);
+ if (NS_FAILED(rv)) {
+ // Skip this scalar if we failed to write it. We don't bail out just
+ // yet as we may salvage other scalars. We eventually need to call
+ // EndObject.
+ continue;
+ }
+ }
+ aWriter.EndObject();
+ }
+ aWriter.EndObject();
+ }
+
+ return NS_OK;
+}
+
+/**
+ * Load the persisted measurements from a Json object and inject them
+ * in the relevant process storage.
+ *
+ * @param {aData} The input Json object.
+ * @returns NS_OK if loading was performed, an error code explaining the
+ * failure reason otherwise.
+ */
+nsresult TelemetryScalar::DeserializePersistedScalars(JSContext* aCx,
+ JS::HandleValue aData) {
+ MOZ_ASSERT(XRE_IsParentProcess(), "Only load scalars in the parent process");
+ if (!XRE_IsParentProcess()) {
+ return NS_ERROR_FAILURE;
+ }
+
+ typedef std::pair<nsCString, nsCOMPtr<nsIVariant>> PersistedScalarPair;
+ typedef nsTArray<PersistedScalarPair> PersistedScalarArray;
+ typedef nsDataHashtable<ProcessIDHashKey, PersistedScalarArray>
+ PeristedScalarStorage;
+
+ PeristedScalarStorage scalarsToUpdate;
+
+ // Before updating the scalars, we need to get the data out of the JS
+ // wrappers. We can't hold the scalars mutex while handling JS stuff.
+ // Build a <scalar name, value> map.
+ JS::RootedObject scalarDataObj(aCx, &aData.toObject());
+ JS::Rooted<JS::IdVector> processes(aCx, JS::IdVector(aCx));
+ if (!JS_Enumerate(aCx, scalarDataObj, &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;
+ }
+
+ // 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, scalarDataObj, process, &processData)) {
+ JS_ClearPendingException(aCx);
+ continue;
+ }
+
+ if (!processData.isObject()) {
+ // |processData| should be an object containing scalars. If this is
+ // not the case, silently skip and try to load the data for the other
+ // processes.
+ continue;
+ }
+
+ // Iterate through each scalar.
+ JS::RootedObject processDataObj(aCx, &processData.toObject());
+ JS::Rooted<JS::IdVector> scalars(aCx, JS::IdVector(aCx));
+ if (!JS_Enumerate(aCx, processDataObj, &scalars)) {
+ JS_ClearPendingException(aCx);
+ continue;
+ }
+
+ JS::RootedId scalar(aCx);
+ for (auto& scalarVal : scalars) {
+ scalar = scalarVal;
+ // Get the scalar name.
+ nsAutoJSString scalarName;
+ if (!scalarName.init(aCx, scalar)) {
+ JS_ClearPendingException(aCx);
+ continue;
+ }
+
+ // Get the scalar value as a JS value.
+ JS::RootedValue scalarValue(aCx);
+ if (!JS_GetPropertyById(aCx, processDataObj, scalar, &scalarValue)) {
+ JS_ClearPendingException(aCx);
+ continue;
+ }
+
+ if (scalarValue.isNullOrUndefined()) {
+ // We can't set scalars to null or undefined values, skip this
+ // and try to load other scalars.
+ continue;
+ }
+
+ // Unpack the aVal to nsIVariant.
+ nsCOMPtr<nsIVariant> unpackedVal;
+ nsresult rv = nsContentUtils::XPConnect()->JSToVariant(
+ aCx, scalarValue, getter_AddRefs(unpackedVal));
+ if (NS_FAILED(rv)) {
+ JS_ClearPendingException(aCx);
+ continue;
+ }
+
+ // Add the scalar to the map.
+ PersistedScalarArray& processScalars =
+ scalarsToUpdate.GetOrInsert(static_cast<uint32_t>(processID));
+ processScalars.AppendElement(std::make_pair(
+ nsCString(NS_ConvertUTF16toUTF8(scalarName)), unpackedVal));
+ }
+ }
+
+ // Now that all the JS specific operations are finished, update the scalars.
+ {
+ StaticMutexAutoLock lock(gTelemetryScalarsMutex);
+
+ for (auto iter = scalarsToUpdate.ConstIter(); !iter.Done(); iter.Next()) {
+ PersistedScalarArray& processScalars = iter.Data();
+ for (PersistedScalarArray::size_type i = 0; i < processScalars.Length();
+ i++) {
+ mozilla::Unused << internal_UpdateScalar(
+ lock, processScalars[i].first, ScalarActionType::eSet,
+ processScalars[i].second, ProcessID(iter.Key()), true /* aForce */);
+ }
+ }
+ }
+
+ return NS_OK;
+}
+
+/**
+ * Load the persisted measurements from a Json object and injects them
+ * in the relevant process storage.
+ *
+ * @param {aData} The input Json object.
+ * @returns NS_OK if loading was performed, an error code explaining the
+ * failure reason otherwise.
+ */
+nsresult TelemetryScalar::DeserializePersistedKeyedScalars(
+ JSContext* aCx, JS::HandleValue aData) {
+ MOZ_ASSERT(XRE_IsParentProcess(), "Only load scalars in the parent process");
+ if (!XRE_IsParentProcess()) {
+ return NS_ERROR_FAILURE;
+ }
+
+ typedef mozilla::Tuple<nsCString, nsString, nsCOMPtr<nsIVariant>>
+ PersistedKeyedScalarTuple;
+ typedef nsTArray<PersistedKeyedScalarTuple> PersistedKeyedScalarArray;
+ typedef nsDataHashtable<ProcessIDHashKey, PersistedKeyedScalarArray>
+ PeristedKeyedScalarStorage;
+
+ PeristedKeyedScalarStorage scalarsToUpdate;
+
+ // Before updating the keyed scalars, we need to get the data out of the JS
+ // wrappers. We can't hold the scalars mutex while handling JS stuff.
+ // Build a <scalar name, value> map.
+ JS::RootedObject scalarDataObj(aCx, &aData.toObject());
+ JS::Rooted<JS::IdVector> processes(aCx, JS::IdVector(aCx));
+ if (!JS_Enumerate(aCx, scalarDataObj, &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;
+ }
+
+ // 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) {
+ 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, scalarDataObj, process, &processData)) {
+ JS_ClearPendingException(aCx);
+ continue;
+ }
+
+ if (!processData.isObject()) {
+ // |processData| should be an object containing scalars. If this is
+ // not the case, silently skip and try to load the data for the other
+ // processes.
+ continue;
+ }
+
+ // Iterate through each keyed scalar.
+ JS::RootedObject processDataObj(aCx, &processData.toObject());
+ JS::Rooted<JS::IdVector> keyedScalars(aCx, JS::IdVector(aCx));
+ if (!JS_Enumerate(aCx, processDataObj, &keyedScalars)) {
+ JS_ClearPendingException(aCx);
+ continue;
+ }
+
+ JS::RootedId keyedScalar(aCx);
+ for (auto& keyedScalarVal : keyedScalars) {
+ keyedScalar = keyedScalarVal;
+ // Get the scalar name.
+ nsAutoJSString scalarName;
+ if (!scalarName.init(aCx, keyedScalar)) {
+ JS_ClearPendingException(aCx);
+ continue;
+ }
+
+ // Get the data for this keyed scalar.
+ JS::RootedValue keyedScalarData(aCx);
+ if (!JS_GetPropertyById(aCx, processDataObj, keyedScalar,
+ &keyedScalarData)) {
+ JS_ClearPendingException(aCx);
+ continue;
+ }
+
+ if (!keyedScalarData.isObject()) {
+ // Keyed scalar data need to be an object. If that's not the case, skip
+ // it and try to load the rest of the data.
+ continue;
+ }
+
+ // Get the keys in the keyed scalar.
+ JS::RootedObject keyedScalarDataObj(aCx, &keyedScalarData.toObject());
+ JS::Rooted<JS::IdVector> keys(aCx, JS::IdVector(aCx));
+ if (!JS_Enumerate(aCx, keyedScalarDataObj, &keys)) {
+ JS_ClearPendingException(aCx);
+ continue;
+ }
+
+ JS::RootedId key(aCx);
+ for (auto keyVal : keys) {
+ key = keyVal;
+ // Get the process name.
+ nsAutoJSString keyName;
+ if (!keyName.init(aCx, key)) {
+ JS_ClearPendingException(aCx);
+ continue;
+ }
+
+ // Get the scalar value as a JS value.
+ JS::RootedValue scalarValue(aCx);
+ if (!JS_GetPropertyById(aCx, keyedScalarDataObj, key, &scalarValue)) {
+ JS_ClearPendingException(aCx);
+ continue;
+ }
+
+ if (scalarValue.isNullOrUndefined()) {
+ // We can't set scalars to null or undefined values, skip this
+ // and try to load other scalars.
+ continue;
+ }
+
+ // Unpack the aVal to nsIVariant.
+ nsCOMPtr<nsIVariant> unpackedVal;
+ nsresult rv = nsContentUtils::XPConnect()->JSToVariant(
+ aCx, scalarValue, getter_AddRefs(unpackedVal));
+ if (NS_FAILED(rv)) {
+ JS_ClearPendingException(aCx);
+ continue;
+ }
+
+ // Add the scalar to the map.
+ PersistedKeyedScalarArray& processScalars =
+ scalarsToUpdate.GetOrInsert(static_cast<uint32_t>(processID));
+ processScalars.AppendElement(
+ mozilla::MakeTuple(nsCString(NS_ConvertUTF16toUTF8(scalarName)),
+ nsString(keyName), unpackedVal));
+ }
+ }
+ }
+
+ // Now that all the JS specific operations are finished, update the scalars.
+ {
+ StaticMutexAutoLock lock(gTelemetryScalarsMutex);
+
+ for (auto iter = scalarsToUpdate.ConstIter(); !iter.Done(); iter.Next()) {
+ PersistedKeyedScalarArray& processScalars = iter.Data();
+ for (PersistedKeyedScalarArray::size_type i = 0;
+ i < processScalars.Length(); i++) {
+ mozilla::Unused << internal_UpdateKeyedScalar(
+ lock, mozilla::Get<0>(processScalars[i]),
+ mozilla::Get<1>(processScalars[i]), ScalarActionType::eSet,
+ mozilla::Get<2>(processScalars[i]), ProcessID(iter.Key()),
+ true /* aForce */);
+ }
+ }
+ }
+
+ return NS_OK;
+}