/* -*- 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(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>> 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& 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 CharPtrEntryType; typedef AutoHashtable ScalarMapType; // Dynamic scalar definitions. StaticAutoPtr> 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(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& 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& 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& 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 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& aResult) final; size_t SizeOfIncludingThis(mozilla::MallocSizeOf aMallocSizeOf) const final; private: nsTArray 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& 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 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& aResult) final; size_t SizeOfIncludingThis(mozilla::MallocSizeOf aMallocSizeOf) const final; private: nsTArray 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& aResult) { nsCOMPtr 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& aResult) final; size_t SizeOfIncludingThis(mozilla::MallocSizeOf aMallocSizeOf) const final; private: nsTArray 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& aResult) { nsCOMPtr 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> 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& 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 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& aValues) { for (auto iter = mScalarKeys.ConstIter(); !iter.Done(); iter.Next()) { ScalarBase* scalar = iter.UserData(); // Get the scalar value. nsCOMPtr 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( 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( 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 ScalarStorageMapType; typedef nsClassHashtable KeyedScalarStorageMapType; typedef nsClassHashtable ProcessesScalarsMapType; typedef nsClassHashtable ProcessesKeyedScalarsMapType; typedef mozilla::Tuple, uint32_t> ScalarDataTuple; typedef nsTArray ScalarTupleArray; typedef nsDataHashtable ScalarSnapshotTable; typedef mozilla::Tuple, uint32_t> KeyedScalarDataTuple; typedef nsTArray KeyedScalarTupleArray; typedef nsDataHashtable 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> gScalarsActions; StaticAutoPtr> 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(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(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(); } // 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(); } // 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 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 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(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 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 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& aDynamicScalarInfos, nsTArray& 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& scalarDefs) { nsTArray 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& scalarInfos) { // Register the new scalars. if (!gDynamicScalarInfo) { gDynamicScalarInfo = new nsTArray(); } if (!gDynamicStoreNames) { gDynamicStoreNames = new nsTArray>(); } 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(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 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(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 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& aScalarActions, const mozilla::Maybe& 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()) { NS_WARNING("Attempting to set a count scalar to a non-integer."); continue; } scalar->SetValue(upd.mData->as()); break; case nsITelemetry::SCALAR_TYPE_BOOLEAN: if (!upd.mData->is()) { NS_WARNING( "Attempting to set a boolean scalar to a non-boolean."); continue; } scalar->SetValue(upd.mData->as()); break; case nsITelemetry::SCALAR_TYPE_STRING: if (!upd.mData->is()) { NS_WARNING("Attempting to set a string scalar to a non-string."); continue; } scalar->SetValue(upd.mData->as()); 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()) { NS_WARNING("Attempting to add to a count scalar with a non-integer."); continue; } scalar->AddValue(upd.mData->as()); 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()) { NS_WARNING( "Attempting to setMaximum a count scalar to a non-integer."); continue; } scalar->SetMaximum(upd.mData->as()); break; } default: NS_WARNING("Unsupported action coming from scalar child updates."); } } } void internal_ApplyKeyedScalarActions( const StaticMutexAutoLock& lock, const nsTArray& aScalarActions, const mozilla::Maybe& 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()) { NS_WARNING("Attempting to set a count scalar to a non-integer."); continue; } scalar->SetValue(lock, NS_ConvertUTF8toUTF16(upd.mKey), upd.mData->as()); break; case nsITelemetry::SCALAR_TYPE_BOOLEAN: if (!upd.mData->is()) { NS_WARNING( "Attempting to set a boolean scalar to a non-boolean."); continue; } scalar->SetValue(lock, NS_ConvertUTF8toUTF16(upd.mKey), upd.mData->as()); 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()) { NS_WARNING("Attempting to add to a count scalar with a non-integer."); continue; } scalar->AddValue(lock, NS_ConvertUTF8toUTF16(upd.mKey), upd.mData->as()); 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()) { NS_WARNING( "Attempting to setMaximum a count scalar to a non-integer."); continue; } scalar->SetMaximum(lock, NS_ConvertUTF8toUTF16(upd.mKey), upd.mData->as()); 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(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 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 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 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(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(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 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 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(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(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(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(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(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 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 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(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(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 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 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 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 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 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& 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 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 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 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 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(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(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(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(value.toBoolean()); } // Get the scalar's optional stores list (default to ["main"]). nsTArray 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 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 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(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& 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& 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 rv; rv = internal_GetScalarByEnum( locker, ScalarKey{ static_cast(ScalarID::TELEMETRY_DISCARDED_ACCUMULATIONS), false}, aProcessType, &scalar); MOZ_ASSERT(NS_SUCCEEDED(rv)); scalar->AddValue(aDiscardedData.mDiscardedHistogramAccumulations); rv = internal_GetScalarByEnum( locker, ScalarKey{static_cast( ScalarID::TELEMETRY_DISCARDED_KEYED_ACCUMULATIONS), false}, aProcessType, &scalar); MOZ_ASSERT(NS_SUCCEEDED(rv)); scalar->AddValue(aDiscardedData.mDiscardedKeyedHistogramAccumulations); rv = internal_GetScalarByEnum( locker, ScalarKey{ static_cast(ScalarID::TELEMETRY_DISCARDED_SCALAR_ACTIONS), false}, aProcessType, &scalar); MOZ_ASSERT(NS_SUCCEEDED(rv)); scalar->AddValue(aDiscardedData.mDiscardedScalarActions); rv = internal_GetScalarByEnum( locker, ScalarKey{static_cast( 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(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& 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& aDefs) { MOZ_ASSERT(!XRE_IsParentProcess()); nsTArray 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& 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> PersistedScalarPair; typedef nsTArray PersistedScalarArray; typedef nsDataHashtable 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 map. JS::RootedObject scalarDataObj(aCx, &aData.toObject()); JS::Rooted 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 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 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 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(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> PersistedKeyedScalarTuple; typedef nsTArray PersistedKeyedScalarArray; typedef nsDataHashtable 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 map. JS::RootedObject scalarDataObj(aCx, &aData.toObject()); JS::Rooted 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 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 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 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(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; }