diff options
Diffstat (limited to 'toolkit/components/telemetry/TelemetryScalar.cpp')
-rw-r--r-- | toolkit/components/telemetry/TelemetryScalar.cpp | 1896 |
1 files changed, 1896 insertions, 0 deletions
diff --git a/toolkit/components/telemetry/TelemetryScalar.cpp b/toolkit/components/telemetry/TelemetryScalar.cpp new file mode 100644 index 000000000..6e9558070 --- /dev/null +++ b/toolkit/components/telemetry/TelemetryScalar.cpp @@ -0,0 +1,1896 @@ +/* -*- 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 "nsITelemetry.h" +#include "nsIVariant.h" +#include "nsVariant.h" +#include "nsHashKeys.h" +#include "nsBaseHashtable.h" +#include "nsClassHashtable.h" +#include "nsIXPConnect.h" +#include "nsContentUtils.h" +#include "nsThreadUtils.h" +#include "mozilla/StaticMutex.h" +#include "mozilla/Unused.h" + +#include "TelemetryCommon.h" +#include "TelemetryScalar.h" +#include "TelemetryScalarData.h" + +using mozilla::StaticMutex; +using mozilla::StaticMutexAutoLock; +using mozilla::Telemetry::Common::AutoHashtable; +using mozilla::Telemetry::Common::IsExpiredVersion; +using mozilla::Telemetry::Common::CanRecordDataset; +using mozilla::Telemetry::Common::IsInDataset; +using mozilla::Telemetry::Common::LogToBrowserConsole; + +//////////////////////////////////////////////////////////////////////// +//////////////////////////////////////////////////////////////////////// +// +// Naming: there are two kinds of functions in this file: +// +// * Functions named internal_*: these can only be reached via an +// interface function (TelemetryScalar::*). They expect the interface +// function to have acquired |gTelemetryScalarsMutex|, so they do not +// have to be thread-safe. +// +// * 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, and many of the +// internal_* functions can ignore thread safety. But it also brings +// a danger of deadlock if any function in the external interface can +// get back to that interface. That is, we will deadlock on any call +// chain like this +// +// 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 kMaximumKeyStringLength = 70; +const uint32_t kMaximumStringValueLength = 50; +const uint32_t kScalarCount = + static_cast<uint32_t>(mozilla::Telemetry::ScalarID::ScalarCount); + +enum class ScalarResult : uint8_t { + // Nothing went wrong. + Ok, + // General Scalar Errors + OperationNotSupported, + InvalidType, + InvalidValue, + // Keyed Scalar Errors + KeyTooLong, + TooManyKeys, + // String Scalar Errors + StringTooLong, + // Unsigned Scalar Errors + UnsignedNegativeValue, + UnsignedTruncatedValue +}; + +typedef nsBaseHashtableET<nsDepCharHashKey, mozilla::Telemetry::ScalarID> + CharPtrEntryType; + +typedef AutoHashtable<CharPtrEntryType> ScalarMapType; + +/** + * Map the error codes used internally to NS_* error codes. + * @param aSr The error code used internally in this module. + * @return {nsresult} A NS_* error code. + */ +nsresult +MapToNsResult(ScalarResult aSr) +{ + switch (aSr) { + case ScalarResult::Ok: + return NS_OK; + case ScalarResult::OperationNotSupported: + return NS_ERROR_NOT_AVAILABLE; + case ScalarResult::StringTooLong: + // We don't want to throw if we're setting a string that is too long. + return NS_OK; + case ScalarResult::InvalidType: + case ScalarResult::InvalidValue: + case ScalarResult::KeyTooLong: + return NS_ERROR_ILLEGAL_VALUE; + case ScalarResult::TooManyKeys: + return NS_ERROR_FAILURE; + case ScalarResult::UnsignedNegativeValue: + case ScalarResult::UnsignedTruncatedValue: + // We shouldn't throw if trying to set a negative number or are truncated, + // only warn the user. + return NS_OK; + } + return NS_ERROR_FAILURE; +} + +bool +IsValidEnumId(mozilla::Telemetry::ScalarID aID) +{ + return aID < mozilla::Telemetry::ScalarID::ScalarCount; +} + +// 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 servers as a common ancestor for storage + * purposes. + */ +class ScalarBase +{ +public: + virtual ~ScalarBase() {}; + + // 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(nsCOMPtr<nsIVariant>& aResult) const = 0; + + // To measure the memory stats. + virtual size_t SizeOfIncludingThis(mozilla::MallocSizeOf aMallocSizeOf) const = 0; + +private: + ScalarResult HandleUnsupported() const; +}; + +ScalarResult +ScalarBase::HandleUnsupported() const +{ + MOZ_ASSERT(false, "This operation is not support for this scalar type."); + return ScalarResult::OperationNotSupported; +} + +/** + * The implementation for the unsigned int scalar type. + */ +class ScalarUnsigned : public ScalarBase +{ +public: + using ScalarBase::SetValue; + + ScalarUnsigned() : mStorage(0) {}; + ~ScalarUnsigned() {}; + + 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(nsCOMPtr<nsIVariant>& aResult) const final; + size_t SizeOfIncludingThis(mozilla::MallocSizeOf aMallocSizeOf) const final; + +private: + uint32_t mStorage; + + ScalarResult CheckInput(nsIVariant* aValue); + + // Prevent copying. + ScalarUnsigned(const ScalarUnsigned& aOther) = delete; + void operator=(const ScalarUnsigned& aOther) = delete; +}; + +ScalarResult +ScalarUnsigned::SetValue(nsIVariant* aValue) +{ + ScalarResult sr = CheckInput(aValue); + if (sr == ScalarResult::UnsignedNegativeValue) { + return sr; + } + + if (NS_FAILED(aValue->GetAsUint32(&mStorage))) { + return ScalarResult::InvalidValue; + } + return sr; +} + +void +ScalarUnsigned::SetValue(uint32_t aValue) +{ + mStorage = aValue; +} + +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; + } + mStorage += newAddend; + return sr; +} + +void +ScalarUnsigned::AddValue(uint32_t aValue) +{ + mStorage += aValue; +} + +ScalarResult +ScalarUnsigned::SetMaximum(nsIVariant* aValue) +{ + 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; + } + if (newValue > mStorage) { + mStorage = newValue; + } + return sr; +} + +void +ScalarUnsigned::SetMaximum(uint32_t aValue) +{ + if (aValue > mStorage) { + mStorage = aValue; + } +} + +nsresult +ScalarUnsigned::GetValue(nsCOMPtr<nsIVariant>& aResult) const +{ + nsCOMPtr<nsIWritableVariant> outVar(new nsVariant()); + nsresult rv = outVar->SetAsUint32(mStorage); + if (NS_FAILED(rv)) { + return rv; + } + aResult = outVar.forget(); + return NS_OK; +} + +size_t +ScalarUnsigned::SizeOfIncludingThis(mozilla::MallocSizeOf aMallocSizeOf) const +{ + return aMallocSizeOf(this); +} + +ScalarResult +ScalarUnsigned::CheckInput(nsIVariant* aValue) +{ + // If this is a floating point value/double, we will probably get truncated. + uint16_t type; + aValue->GetDataType(&type); + 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; + + ScalarString() : mStorage(EmptyString()) {}; + ~ScalarString() {}; + + ScalarResult SetValue(nsIVariant* aValue) final; + ScalarResult SetValue(const nsAString& aValue) final; + nsresult GetValue(nsCOMPtr<nsIVariant>& aResult) const final; + size_t SizeOfIncludingThis(mozilla::MallocSizeOf aMallocSizeOf) const final; + +private: + nsString mStorage; + + // Prevent copying. + ScalarString(const ScalarString& aOther) = delete; + void operator=(const ScalarString& aOther) = delete; +}; + +ScalarResult +ScalarString::SetValue(nsIVariant* aValue) +{ + // Check that we got the correct data type. + uint16_t type; + aValue->GetDataType(&type); + if (type != nsIDataType::VTYPE_CHAR && + type != nsIDataType::VTYPE_WCHAR && + type != nsIDataType::VTYPE_DOMSTRING && + 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) +{ + mStorage = Substring(aValue, 0, kMaximumStringValueLength); + if (aValue.Length() > kMaximumStringValueLength) { + return ScalarResult::StringTooLong; + } + return ScalarResult::Ok; +} + +nsresult +ScalarString::GetValue(nsCOMPtr<nsIVariant>& aResult) const +{ + nsCOMPtr<nsIWritableVariant> outVar(new nsVariant()); + nsresult rv = outVar->SetAsAString(mStorage); + if (NS_FAILED(rv)) { + return rv; + } + aResult = outVar.forget(); + return NS_OK; +} + +size_t +ScalarString::SizeOfIncludingThis(mozilla::MallocSizeOf aMallocSizeOf) const +{ + size_t n = aMallocSizeOf(this); + n+= mStorage.SizeOfExcludingThisIfUnshared(aMallocSizeOf); + return n; +} + +/** + * The implementation for the boolean scalar type. + */ +class ScalarBoolean : public ScalarBase +{ +public: + using ScalarBase::SetValue; + + ScalarBoolean() : mStorage(false) {}; + ~ScalarBoolean() {}; + + ScalarResult SetValue(nsIVariant* aValue) final; + void SetValue(bool aValue) final; + nsresult GetValue(nsCOMPtr<nsIVariant>& aResult) const final; + size_t SizeOfIncludingThis(mozilla::MallocSizeOf aMallocSizeOf) const final; + +private: + bool mStorage; + + // Prevent copying. + ScalarBoolean(const ScalarBoolean& aOther) = delete; + void operator=(const ScalarBoolean& aOther) = delete; +}; + +ScalarResult +ScalarBoolean::SetValue(nsIVariant* aValue) +{ + // Check that we got the correct data type. + uint16_t type; + aValue->GetDataType(&type); + 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; + } + + if (NS_FAILED(aValue->GetAsBool(&mStorage))) { + return ScalarResult::InvalidValue; + } + return ScalarResult::Ok; +}; + +void +ScalarBoolean::SetValue(bool aValue) +{ + mStorage = aValue; +} + +nsresult +ScalarBoolean::GetValue(nsCOMPtr<nsIVariant>& aResult) const +{ + nsCOMPtr<nsIWritableVariant> outVar(new nsVariant()); + nsresult rv = outVar->SetAsBool(mStorage); + if (NS_FAILED(rv)) { + return rv; + } + aResult = outVar.forget(); + return NS_OK; +} + +size_t +ScalarBoolean::SizeOfIncludingThis(mozilla::MallocSizeOf aMallocSizeOf) const +{ + return aMallocSizeOf(this); +} + +/** + * 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(uint32_t aScalarKind) +{ + ScalarBase* scalar = nullptr; + switch (aScalarKind) { + case nsITelemetry::SCALAR_COUNT: + scalar = new ScalarUnsigned(); + break; + case nsITelemetry::SCALAR_STRING: + scalar = new ScalarString(); + break; + case nsITelemetry::SCALAR_BOOLEAN: + scalar = new ScalarBoolean(); + break; + default: + MOZ_ASSERT(false, "Invalid scalar type"); + } + return scalar; +} + +/** + * The implementation for the keyed scalar type. + */ +class KeyedScalar +{ +public: + typedef mozilla::Pair<nsCString, nsCOMPtr<nsIVariant>> KeyValuePair; + + explicit KeyedScalar(uint32_t aScalarKind) : mScalarKind(aScalarKind) {}; + ~KeyedScalar() {}; + + // Set, Add and SetMaximum functions as described in the Telemetry IDL. + // These methods implicitly instantiate a Scalar[*] for each key. + ScalarResult SetValue(const nsAString& aKey, nsIVariant* aValue); + ScalarResult AddValue(const nsAString& aKey, nsIVariant* aValue); + ScalarResult SetMaximum(const nsAString& aKey, nsIVariant* aValue); + + // Convenience methods used by the C++ API. + void SetValue(const nsAString& aKey, uint32_t aValue); + void SetValue(const nsAString& aKey, bool aValue); + void AddValue(const nsAString& aKey, uint32_t aValue); + void SetMaximum(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(nsTArray<KeyValuePair>& aValues) const; + + // To measure the memory stats. + size_t SizeOfIncludingThis(mozilla::MallocSizeOf aMallocSizeOf); + +private: + typedef nsClassHashtable<nsCStringHashKey, ScalarBase> ScalarKeysMapType; + + ScalarKeysMapType mScalarKeys; + const uint32_t mScalarKind; + + ScalarResult GetScalarForKey(const nsAString& aKey, ScalarBase** aRet); +}; + +ScalarResult +KeyedScalar::SetValue(const nsAString& aKey, nsIVariant* aValue) +{ + ScalarBase* scalar = nullptr; + ScalarResult sr = GetScalarForKey(aKey, &scalar); + if (sr != ScalarResult::Ok) { + return sr; + } + + return scalar->SetValue(aValue); +} + +ScalarResult +KeyedScalar::AddValue(const nsAString& aKey, nsIVariant* aValue) +{ + ScalarBase* scalar = nullptr; + ScalarResult sr = GetScalarForKey(aKey, &scalar); + if (sr != ScalarResult::Ok) { + return sr; + } + + return scalar->AddValue(aValue); +} + +ScalarResult +KeyedScalar::SetMaximum(const nsAString& aKey, nsIVariant* aValue) +{ + ScalarBase* scalar = nullptr; + ScalarResult sr = GetScalarForKey(aKey, &scalar); + if (sr != ScalarResult::Ok) { + return sr; + } + + return scalar->SetMaximum(aValue); +} + +void +KeyedScalar::SetValue(const nsAString& aKey, uint32_t aValue) +{ + ScalarBase* scalar = nullptr; + ScalarResult sr = GetScalarForKey(aKey, &scalar); + if (sr != ScalarResult::Ok) { + MOZ_ASSERT(false, "Key too long or too many keys are recorded in the scalar."); + return; + } + + return scalar->SetValue(aValue); +} + +void +KeyedScalar::SetValue(const nsAString& aKey, bool aValue) +{ + ScalarBase* scalar = nullptr; + ScalarResult sr = GetScalarForKey(aKey, &scalar); + if (sr != ScalarResult::Ok) { + MOZ_ASSERT(false, "Key too long or too many keys are recorded in the scalar."); + return; + } + + return scalar->SetValue(aValue); +} + +void +KeyedScalar::AddValue(const nsAString& aKey, uint32_t aValue) +{ + ScalarBase* scalar = nullptr; + ScalarResult sr = GetScalarForKey(aKey, &scalar); + if (sr != ScalarResult::Ok) { + MOZ_ASSERT(false, "Key too long or too many keys are recorded in the scalar."); + return; + } + + return scalar->AddValue(aValue); +} + +void +KeyedScalar::SetMaximum(const nsAString& aKey, uint32_t aValue) +{ + ScalarBase* scalar = nullptr; + ScalarResult sr = GetScalarForKey(aKey, &scalar); + if (sr != ScalarResult::Ok) { + MOZ_ASSERT(false, "Key too long or too many keys are 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(nsTArray<KeyValuePair>& aValues) const +{ + for (auto iter = mScalarKeys.ConstIter(); !iter.Done(); iter.Next()) { + ScalarBase* scalar = static_cast<ScalarBase*>(iter.Data()); + + // Get the scalar value. + nsCOMPtr<nsIVariant> scalarValue; + nsresult rv = scalar->GetValue(scalarValue); + if (NS_FAILED(rv)) { + return rv; + } + + // Append it to value list. + aValues.AppendElement(mozilla::MakePair(nsCString(iter.Key()), scalarValue)); + } + + return NS_OK; +} + +/** + * 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 nsAString& aKey, ScalarBase** aRet) +{ + if (aKey.Length() >= kMaximumKeyStringLength) { + return ScalarResult::KeyTooLong; + } + + if (mScalarKeys.Count() >= kMaximumNumberOfKeys) { + return ScalarResult::TooManyKeys; + } + + NS_ConvertUTF16toUTF8 utf8Key(aKey); + + ScalarBase* scalar = nullptr; + if (mScalarKeys.Get(utf8Key, &scalar)) { + *aRet = scalar; + return ScalarResult::Ok; + } + + scalar = internal_ScalarAllocate(mScalarKind); + 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 = static_cast<ScalarBase*>(iter.Data()); + n += scalar->SizeOfIncludingThis(aMallocSizeOf); + } + return n; +} + +typedef nsUint32HashKey ScalarIDHashKey; +typedef nsClassHashtable<ScalarIDHashKey, ScalarBase> ScalarStorageMapType; +typedef nsClassHashtable<ScalarIDHashKey, KeyedScalar> KeyedScalarStorageMapType; + +} // 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 ID -> Scalar Object map. This is a nsClassHashtable, it owns +// the scalar instance and takes care of deallocating them when they +// get removed from the map. +ScalarStorageMapType gScalarStorageMap; +// The ID -> Keyed Scalar Object map. As for plain scalars, this is +// nsClassHashtable. See above. +KeyedScalarStorageMapType gKeyedScalarStorageMap; + +} // 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 { + +/** + * Checks if the error should be logged. + * + * @param aSr The error code. + * @return true if the error should be logged, false otherwise. + */ +bool +internal_ShouldLogError(ScalarResult aSr) +{ + switch (aSr) { + case ScalarResult::StringTooLong: MOZ_FALLTHROUGH; + case ScalarResult::KeyTooLong: MOZ_FALLTHROUGH; + case ScalarResult::TooManyKeys: MOZ_FALLTHROUGH; + case ScalarResult::UnsignedNegativeValue: MOZ_FALLTHROUGH; + case ScalarResult::UnsignedTruncatedValue: + // Intentional fall-through. + return true; + + default: + return false; + } + + // It should never reach this point. + return false; +} + +/** + * 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::StringTooLong: + errorMessage.Append(NS_LITERAL_STRING(" - Truncating scalar value to 50 characters.")); + break; + case ScalarResult::KeyTooLong: + errorMessage.Append(NS_LITERAL_STRING(" - The key length must be limited to 70 characters.")); + break; + case ScalarResult::TooManyKeys: + errorMessage.Append(NS_LITERAL_STRING(" - Keyed scalars cannot have more than 100 keys.")); + break; + case ScalarResult::UnsignedNegativeValue: + errorMessage.Append(NS_LITERAL_STRING(" - Trying to set an unsigned scalar to a negative number.")); + break; + case ScalarResult::UnsignedTruncatedValue: + errorMessage.Append(NS_LITERAL_STRING(" - Truncating float/double number.")); + break; + default: + // Nothing. + return; + } + + LogToBrowserConsole(nsIScriptError::warningFlag, errorMessage); +} + +} // namespace + +//////////////////////////////////////////////////////////////////////// +//////////////////////////////////////////////////////////////////////// +// +// PRIVATE: thread-unsafe helpers for the external interface + +namespace { + +bool +internal_CanRecordBase() +{ + return gCanRecordBase; +} + +bool +internal_CanRecordExtended() +{ + return gCanRecordExtended; +} + +const ScalarInfo& +internal_InfoForScalarID(mozilla::Telemetry::ScalarID aId) +{ + return gScalars[static_cast<uint32_t>(aId)]; +} + +/** + * Check if the given scalar is a keyed scalar. + * + * @param aId The scalar enum. + * @return true if aId refers to a keyed scalar, false otherwise. + */ +bool +internal_IsKeyedScalar(mozilla::Telemetry::ScalarID aId) +{ + return internal_InfoForScalarID(aId).keyed; +} + +bool +internal_CanRecordForScalarID(mozilla::Telemetry::ScalarID aId) +{ + // Get the scalar info from the id. + const ScalarInfo &info = internal_InfoForScalarID(aId); + + // Can we record at all? + bool canRecordBase = internal_CanRecordBase(); + if (!canRecordBase) { + return false; + } + + bool canRecordDataset = CanRecordDataset(info.dataset, + canRecordBase, + internal_CanRecordExtended()); + if (!canRecordDataset) { + return false; + } + + return true; +} + +/** + * Get the scalar enum id from the scalar name. + * + * @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 nsACString& aName, mozilla::Telemetry::ScalarID* aId) +{ + if (!gInitDone) { + return NS_ERROR_FAILURE; + } + + CharPtrEntryType *entry = gScalarNameIDMap.GetEntry(PromiseFlatCString(aName).get()); + if (!entry) { + return NS_ERROR_INVALID_ARG; + } + *aId = entry->mData; + 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 aId The scalar id. + * @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(mozilla::Telemetry::ScalarID aId, ScalarBase** aRet) +{ + if (!IsValidEnumId(aId)) { + MOZ_ASSERT(false, "Requested a scalar with an invalid id."); + return NS_ERROR_INVALID_ARG; + } + + const uint32_t id = static_cast<uint32_t>(aId); + + ScalarBase* scalar = nullptr; + if (gScalarStorageMap.Get(id, &scalar)) { + *aRet = scalar; + return NS_OK; + } + + const ScalarInfo &info = gScalars[id]; + + if (IsExpiredVersion(info.expiration())) { + return NS_ERROR_NOT_AVAILABLE; + } + + scalar = internal_ScalarAllocate(info.kind); + if (!scalar) { + return NS_ERROR_INVALID_ARG; + } + + gScalarStorageMap.Put(id, scalar); + + *aRet = scalar; + return NS_OK; +} + +/** + * Get a scalar object by its enum id, if we're allowed to record it. + * + * @param aId The scalar id. + * @return The ScalarBase instance or nullptr if we're not allowed to record + * the scalar. + */ +ScalarBase* +internal_GetRecordableScalar(mozilla::Telemetry::ScalarID aId) +{ + // Get the scalar by the enum (it also internally checks for aId validity). + ScalarBase* scalar = nullptr; + nsresult rv = internal_GetScalarByEnum(aId, &scalar); + if (NS_FAILED(rv)) { + return nullptr; + } + + if (internal_IsKeyedScalar(aId)) { + return nullptr; + } + + // Are we allowed to record this scalar? + if (!internal_CanRecordForScalarID(aId)) { + return nullptr; + } + + return scalar; +} + +} // 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 aId The scalar id. + * @param aRes 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(mozilla::Telemetry::ScalarID aId, KeyedScalar** aRet) +{ + if (!IsValidEnumId(aId)) { + MOZ_ASSERT(false, "Requested a keyed scalar with an invalid id."); + return NS_ERROR_INVALID_ARG; + } + + const uint32_t id = static_cast<uint32_t>(aId); + + KeyedScalar* scalar = nullptr; + if (gKeyedScalarStorageMap.Get(id, &scalar)) { + *aRet = scalar; + return NS_OK; + } + + const ScalarInfo &info = gScalars[id]; + + if (IsExpiredVersion(info.expiration())) { + return NS_ERROR_NOT_AVAILABLE; + } + + // We don't currently support keyed string scalars. Disable them. + if (info.kind == nsITelemetry::SCALAR_STRING) { + MOZ_ASSERT(false, "Keyed string scalars are not currently supported."); + return NS_ERROR_INVALID_ARG; + } + + scalar = new KeyedScalar(info.kind); + if (!scalar) { + return NS_ERROR_INVALID_ARG; + } + + gKeyedScalarStorageMap.Put(id, scalar); + + *aRet = scalar; + return NS_OK; +} + +/** + * Get a keyed scalar object by its enum id, if we're allowed to record it. + * + * @param aId The scalar id. + * @return The KeyedScalar instance or nullptr if we're not allowed to record + * the scalar. + */ +KeyedScalar* +internal_GetRecordableKeyedScalar(mozilla::Telemetry::ScalarID aId) +{ + // Get the scalar by the enum (it also internally checks for aId validity). + KeyedScalar* scalar = nullptr; + nsresult rv = internal_GetKeyedScalarByEnum(aId, &scalar); + if (NS_FAILED(rv)) { + return nullptr; + } + + if (!internal_IsKeyedScalar(aId)) { + return nullptr; + } + + // Are we allowed to record this scalar? + if (!internal_CanRecordForScalarID(aId)) { + return nullptr; + } + + return scalar; +} + +} // namespace + +//////////////////////////////////////////////////////////////////////// +//////////////////////////////////////////////////////////////////////// +// +// EXTERNALLY VISIBLE FUNCTIONS in namespace TelemetryScalars:: + +// This is a StaticMutex rather than a plain Mutex (1) so that +// it gets initialised in a thread-safe manner the first time +// it is used, and (2) because it is never de-initialised, and +// a normal Mutex would show up as a leak in BloatView. StaticMutex +// also has the "OffTheBooks" property, so it won't show as a leak +// in BloatView. +// Another reason to use a StaticMutex instead of a plain Mutex is +// that, due to the nature of Telemetry, we cannot rely on having a +// mutex initialized in InitializeGlobalState. Unfortunately, we +// cannot make sure that no other function is called before this point. +static StaticMutex gTelemetryScalarsMutex; + +void +TelemetryScalar::InitializeGlobalState(bool aCanRecordBase, bool aCanRecordExtended) +{ + StaticMutexAutoLock locker(gTelemetryScalarsMutex); + MOZ_ASSERT(!gInitDone, "TelemetryScalar::InitializeGlobalState " + "may only be called once"); + + gCanRecordBase = aCanRecordBase; + gCanRecordExtended = aCanRecordExtended; + + // Populate the static scalar name->id cache. Note that the scalar names are + // statically allocated and come from the automatically generated TelemetryScalarData.h. + uint32_t scalarCount = static_cast<uint32_t>(mozilla::Telemetry::ScalarID::ScalarCount); + for (uint32_t i = 0; i < scalarCount; i++) { + CharPtrEntryType *entry = gScalarNameIDMap.PutEntry(gScalars[i].name()); + entry->mData = static_cast<mozilla::Telemetry::ScalarID>(i); + } + +#ifdef DEBUG + gScalarNameIDMap.MarkImmutable(); +#endif + gInitDone = true; +} + +void +TelemetryScalar::DeInitializeGlobalState() +{ + StaticMutexAutoLock locker(gTelemetryScalarsMutex); + gCanRecordBase = false; + gCanRecordExtended = false; + gScalarNameIDMap.Clear(); + gScalarStorageMap.Clear(); + gKeyedScalarStorageMap.Clear(); + gInitDone = false; +} + +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 if the value was added or if we're not allowed to record to this + * dataset. Otherwise, return an error. + */ +nsresult +TelemetryScalar::Add(const nsACString& aName, JS::HandleValue aVal, JSContext* aCx) +{ + // Unpack the aVal to nsIVariant. This uses the JS context. + nsCOMPtr<nsIVariant> unpackedVal; + nsresult rv = + nsContentUtils::XPConnect()->JSToVariant(aCx, aVal, getter_AddRefs(unpackedVal)); + if (NS_FAILED(rv)) { + return rv; + } + + ScalarResult sr; + { + StaticMutexAutoLock locker(gTelemetryScalarsMutex); + + mozilla::Telemetry::ScalarID id; + rv = internal_GetEnumByScalarName(aName, &id); + if (NS_FAILED(rv)) { + return rv; + } + + // We're trying to set a plain scalar, so make sure this is one. + if (internal_IsKeyedScalar(id)) { + return NS_ERROR_ILLEGAL_VALUE; + } + + // Are we allowed to record this scalar? + if (!internal_CanRecordForScalarID(id)) { + return NS_OK; + } + + // Finally get the scalar. + ScalarBase* scalar = nullptr; + rv = internal_GetScalarByEnum(id, &scalar); + if (NS_FAILED(rv)) { + // Don't throw on expired scalars. + if (rv == NS_ERROR_NOT_AVAILABLE) { + return NS_OK; + } + return rv; + } + + sr = scalar->AddValue(unpackedVal); + } + + // Warn the user about the error if we need to. + if (internal_ShouldLogError(sr)) { + internal_LogScalarError(aName, sr); + } + + return MapToNsResult(sr); +} + +/** + * 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 if the value was added or if we're not allow to record to this + * dataset. Otherwise, return an error. + */ +nsresult +TelemetryScalar::Add(const nsACString& aName, const nsAString& aKey, JS::HandleValue aVal, + JSContext* aCx) +{ + // Unpack the aVal to nsIVariant. This uses the JS context. + nsCOMPtr<nsIVariant> unpackedVal; + nsresult rv = + nsContentUtils::XPConnect()->JSToVariant(aCx, aVal, getter_AddRefs(unpackedVal)); + if (NS_FAILED(rv)) { + return rv; + } + + ScalarResult sr; + { + StaticMutexAutoLock locker(gTelemetryScalarsMutex); + + mozilla::Telemetry::ScalarID id; + rv = internal_GetEnumByScalarName(aName, &id); + if (NS_FAILED(rv)) { + return rv; + } + + // Make sure this is a keyed scalar. + if (!internal_IsKeyedScalar(id)) { + return NS_ERROR_ILLEGAL_VALUE; + } + + // Are we allowed to record this scalar? + if (!internal_CanRecordForScalarID(id)) { + return NS_OK; + } + + // Finally get the scalar. + KeyedScalar* scalar = nullptr; + rv = internal_GetKeyedScalarByEnum(id, &scalar); + if (NS_FAILED(rv)) { + // Don't throw on expired scalars. + if (rv == NS_ERROR_NOT_AVAILABLE) { + return NS_OK; + } + return rv; + } + + sr = scalar->AddValue(aKey, unpackedVal); + } + + // Warn the user about the error if we need to. + if (internal_ShouldLogError(sr)) { + internal_LogScalarError(aName, sr); + } + + return MapToNsResult(sr); +} + +/** + * 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) +{ + StaticMutexAutoLock locker(gTelemetryScalarsMutex); + + ScalarBase* scalar = internal_GetRecordableScalar(aId); + if (!scalar) { + 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) +{ + StaticMutexAutoLock locker(gTelemetryScalarsMutex); + + KeyedScalar* scalar = internal_GetRecordableKeyedScalar(aId); + if (!scalar) { + return; + } + + scalar->AddValue(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 if the value was added or if we're not allow to record to this + * dataset. Otherwise, return an error. + */ +nsresult +TelemetryScalar::Set(const nsACString& aName, JS::HandleValue aVal, JSContext* aCx) +{ + // Unpack the aVal to nsIVariant. This uses the JS context. + nsCOMPtr<nsIVariant> unpackedVal; + nsresult rv = + nsContentUtils::XPConnect()->JSToVariant(aCx, aVal, getter_AddRefs(unpackedVal)); + if (NS_FAILED(rv)) { + return rv; + } + + ScalarResult sr; + { + StaticMutexAutoLock locker(gTelemetryScalarsMutex); + + mozilla::Telemetry::ScalarID id; + rv = internal_GetEnumByScalarName(aName, &id); + if (NS_FAILED(rv)) { + return rv; + } + + // We're trying to set a plain scalar, so make sure this is one. + if (internal_IsKeyedScalar(id)) { + return NS_ERROR_ILLEGAL_VALUE; + } + + // Are we allowed to record this scalar? + if (!internal_CanRecordForScalarID(id)) { + return NS_OK; + } + + // Finally get the scalar. + ScalarBase* scalar = nullptr; + rv = internal_GetScalarByEnum(id, &scalar); + if (NS_FAILED(rv)) { + // Don't throw on expired scalars. + if (rv == NS_ERROR_NOT_AVAILABLE) { + return NS_OK; + } + return rv; + } + + sr = scalar->SetValue(unpackedVal); + } + + // Warn the user about the error if we need to. + if (internal_ShouldLogError(sr)) { + internal_LogScalarError(aName, sr); + } + + return MapToNsResult(sr); +} + +/** + * 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 if the value was added or if we're not allow to record to this + * dataset. Otherwise, return an error. + */ +nsresult +TelemetryScalar::Set(const nsACString& aName, const nsAString& aKey, JS::HandleValue aVal, + JSContext* aCx) +{ + // Unpack the aVal to nsIVariant. This uses the JS context. + nsCOMPtr<nsIVariant> unpackedVal; + nsresult rv = + nsContentUtils::XPConnect()->JSToVariant(aCx, aVal, getter_AddRefs(unpackedVal)); + if (NS_FAILED(rv)) { + return rv; + } + + ScalarResult sr; + { + StaticMutexAutoLock locker(gTelemetryScalarsMutex); + + mozilla::Telemetry::ScalarID id; + rv = internal_GetEnumByScalarName(aName, &id); + if (NS_FAILED(rv)) { + return rv; + } + + // We're trying to set a keyed scalar. Report an error if this isn't one. + if (!internal_IsKeyedScalar(id)) { + return NS_ERROR_ILLEGAL_VALUE; + } + + // Are we allowed to record this scalar? + if (!internal_CanRecordForScalarID(id)) { + return NS_OK; + } + + // Finally get the scalar. + KeyedScalar* scalar = nullptr; + rv = internal_GetKeyedScalarByEnum(id, &scalar); + if (NS_FAILED(rv)) { + // Don't throw on expired scalars. + if (rv == NS_ERROR_NOT_AVAILABLE) { + return NS_OK; + } + return rv; + } + + sr = scalar->SetValue(aKey, unpackedVal); + } + + // Warn the user about the error if we need to. + if (internal_ShouldLogError(sr)) { + internal_LogScalarError(aName, sr); + } + + return MapToNsResult(sr); +} + +/** + * 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) +{ + StaticMutexAutoLock locker(gTelemetryScalarsMutex); + + ScalarBase* scalar = internal_GetRecordableScalar(aId); + if (!scalar) { + 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) +{ + StaticMutexAutoLock locker(gTelemetryScalarsMutex); + + ScalarBase* scalar = internal_GetRecordableScalar(aId); + if (!scalar) { + 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) +{ + StaticMutexAutoLock locker(gTelemetryScalarsMutex); + + ScalarBase* scalar = internal_GetRecordableScalar(aId); + if (!scalar) { + 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) +{ + StaticMutexAutoLock locker(gTelemetryScalarsMutex); + + KeyedScalar* scalar = internal_GetRecordableKeyedScalar(aId); + if (!scalar) { + return; + } + + scalar->SetValue(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) +{ + StaticMutexAutoLock locker(gTelemetryScalarsMutex); + + KeyedScalar* scalar = internal_GetRecordableKeyedScalar(aId); + if (!scalar) { + return; + } + + scalar->SetValue(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 if the value was added or if we're not allow to record to this + * dataset. Otherwise, return an error. + */ +nsresult +TelemetryScalar::SetMaximum(const nsACString& aName, JS::HandleValue aVal, JSContext* aCx) +{ + // Unpack the aVal to nsIVariant. This uses the JS context. + nsCOMPtr<nsIVariant> unpackedVal; + nsresult rv = + nsContentUtils::XPConnect()->JSToVariant(aCx, aVal, getter_AddRefs(unpackedVal)); + if (NS_FAILED(rv)) { + return rv; + } + + ScalarResult sr; + { + StaticMutexAutoLock locker(gTelemetryScalarsMutex); + + mozilla::Telemetry::ScalarID id; + rv = internal_GetEnumByScalarName(aName, &id); + if (NS_FAILED(rv)) { + return rv; + } + + // Make sure this is not a keyed scalar. + if (internal_IsKeyedScalar(id)) { + return NS_ERROR_ILLEGAL_VALUE; + } + + // Are we allowed to record this scalar? + if (!internal_CanRecordForScalarID(id)) { + return NS_OK; + } + + // Finally get the scalar. + ScalarBase* scalar = nullptr; + rv = internal_GetScalarByEnum(id, &scalar); + if (NS_FAILED(rv)) { + // Don't throw on expired scalars. + if (rv == NS_ERROR_NOT_AVAILABLE) { + return NS_OK; + } + return rv; + } + + sr = scalar->SetMaximum(unpackedVal); + } + + // Warn the user about the error if we need to. + if (internal_ShouldLogError(sr)) { + internal_LogScalarError(aName, sr); + } + + return MapToNsResult(sr); +} + +/** + * 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 if the value was added or if we're not allow to record to this + * dataset. Otherwise, return an error. + */ +nsresult +TelemetryScalar::SetMaximum(const nsACString& aName, const nsAString& aKey, JS::HandleValue aVal, + JSContext* aCx) +{ + // Unpack the aVal to nsIVariant. This uses the JS context. + nsCOMPtr<nsIVariant> unpackedVal; + nsresult rv = + nsContentUtils::XPConnect()->JSToVariant(aCx, aVal, getter_AddRefs(unpackedVal)); + if (NS_FAILED(rv)) { + return rv; + } + + ScalarResult sr; + { + StaticMutexAutoLock locker(gTelemetryScalarsMutex); + + mozilla::Telemetry::ScalarID id; + rv = internal_GetEnumByScalarName(aName, &id); + if (NS_FAILED(rv)) { + return rv; + } + + // Make sure this is a keyed scalar. + if (!internal_IsKeyedScalar(id)) { + return NS_ERROR_ILLEGAL_VALUE; + } + + // Are we allowed to record this scalar? + if (!internal_CanRecordForScalarID(id)) { + return NS_OK; + } + + // Finally get the scalar. + KeyedScalar* scalar = nullptr; + rv = internal_GetKeyedScalarByEnum(id, &scalar); + if (NS_FAILED(rv)) { + // Don't throw on expired scalars. + if (rv == NS_ERROR_NOT_AVAILABLE) { + return NS_OK; + } + return rv; + } + + sr = scalar->SetMaximum(aKey, unpackedVal); + } + + // Warn the user about the error if we need to. + if (internal_ShouldLogError(sr)) { + internal_LogScalarError(aName, sr); + } + + return MapToNsResult(sr); +} + +/** + * 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) +{ + StaticMutexAutoLock locker(gTelemetryScalarsMutex); + + ScalarBase* scalar = internal_GetRecordableScalar(aId); + if (!scalar) { + 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) +{ + StaticMutexAutoLock locker(gTelemetryScalarsMutex); + + KeyedScalar* scalar = internal_GetRecordableKeyedScalar(aId); + if (!scalar) { + return; + } + + scalar->SetMaximum(aKey, aValue); +} + +/** + * Serializes the scalars from the given dataset to a json-style object and resets them. + * The returned structure looks like {"group1.probe":1,"group1.other_probe":false,...}. + * + * @param aDataset DATASET_RELEASE_CHANNEL_OPTOUT or DATASET_RELEASE_CHANNEL_OPTIN. + * @param aClear Whether to clear out the scalars after snapshotting. + */ +nsresult +TelemetryScalar::CreateSnapshots(unsigned int aDataset, bool aClearScalars, JSContext* aCx, + uint8_t optional_argc, JS::MutableHandle<JS::Value> aResult) +{ + // If no arguments were passed in, apply the default value. + if (!optional_argc) { + aClearScalars = false; + } + + JS::Rooted<JSObject*> root_obj(aCx, JS_NewPlainObject(aCx)); + if (!root_obj) { + return NS_ERROR_FAILURE; + } + aResult.setObject(*root_obj); + + // Only lock the mutex while accessing our data, without locking any JS related code. + typedef mozilla::Pair<const char*, nsCOMPtr<nsIVariant>> DataPair; + nsTArray<DataPair> scalarsToReflect; + { + StaticMutexAutoLock locker(gTelemetryScalarsMutex); + // Iterate the scalars in gScalarStorageMap. The storage may contain empty or yet to be + // initialized scalars. + for (auto iter = gScalarStorageMap.Iter(); !iter.Done(); iter.Next()) { + ScalarBase* scalar = static_cast<ScalarBase*>(iter.Data()); + + // Get the informations for this scalar. + const ScalarInfo& info = gScalars[iter.Key()]; + + // Serialize the scalar if it's in the desired dataset. + if (IsInDataset(info.dataset, aDataset)) { + // Get the scalar value. + nsCOMPtr<nsIVariant> scalarValue; + nsresult rv = scalar->GetValue(scalarValue); + if (NS_FAILED(rv)) { + return rv; + } + // Append it to our list. + scalarsToReflect.AppendElement(mozilla::MakePair(info.name(), scalarValue)); + } + } + + if (aClearScalars) { + // The map already takes care of freeing the allocated memory. + gScalarStorageMap.Clear(); + } + } + + // Reflect it to JS. + for (nsTArray<DataPair>::size_type i = 0; i < scalarsToReflect.Length(); i++) { + const DataPair& scalar = scalarsToReflect[i]; + + // Convert it to a JS Val. + JS::Rooted<JS::Value> scalarJsValue(aCx); + nsresult rv = + nsContentUtils::XPConnect()->VariantToJS(aCx, root_obj, scalar.second(), &scalarJsValue); + if (NS_FAILED(rv)) { + return rv; + } + + // Add it to the scalar object. + if (!JS_DefineProperty(aCx, root_obj, scalar.first(), scalarJsValue, JSPROP_ENUMERATE)) { + return NS_ERROR_FAILURE; + } + } + + return NS_OK; +} + +/** + * Serializes the scalars from the given dataset to a json-style object and resets them. + * The returned structure looks like: + * { "group1.probe": { "key_1": 2, "key_2": 1, ... }, ... } + * + * @param aDataset DATASET_RELEASE_CHANNEL_OPTOUT or DATASET_RELEASE_CHANNEL_OPTIN. + * @param aClear Whether to clear out the keyed scalars after snapshotting. + */ +nsresult +TelemetryScalar::CreateKeyedSnapshots(unsigned int aDataset, bool aClearScalars, JSContext* aCx, + uint8_t optional_argc, JS::MutableHandle<JS::Value> aResult) +{ + // If no arguments were passed in, apply the default value. + if (!optional_argc) { + aClearScalars = false; + } + + JS::Rooted<JSObject*> root_obj(aCx, JS_NewPlainObject(aCx)); + if (!root_obj) { + return NS_ERROR_FAILURE; + } + aResult.setObject(*root_obj); + + // Only lock the mutex while accessing our data, without locking any JS related code. + typedef mozilla::Pair<const char*, nsTArray<KeyedScalar::KeyValuePair>> DataPair; + nsTArray<DataPair> scalarsToReflect; + { + StaticMutexAutoLock locker(gTelemetryScalarsMutex); + // Iterate the scalars in gKeyedScalarStorageMap. The storage may contain empty or yet + // to be initialized scalars. + for (auto iter = gKeyedScalarStorageMap.Iter(); !iter.Done(); iter.Next()) { + KeyedScalar* scalar = static_cast<KeyedScalar*>(iter.Data()); + + // Get the informations for this scalar. + const ScalarInfo& info = gScalars[iter.Key()]; + + // Serialize the scalar if it's in the desired dataset. + if (IsInDataset(info.dataset, aDataset)) { + // Get the keys for this scalar. + nsTArray<KeyedScalar::KeyValuePair> scalarKeyedData; + nsresult rv = scalar->GetValue(scalarKeyedData); + if (NS_FAILED(rv)) { + return rv; + } + // Append it to our list. + scalarsToReflect.AppendElement(mozilla::MakePair(info.name(), scalarKeyedData)); + } + } + + if (aClearScalars) { + // The map already takes care of freeing the allocated memory. + gKeyedScalarStorageMap.Clear(); + } + } + + // Reflect it to JS. + for (nsTArray<DataPair>::size_type i = 0; i < scalarsToReflect.Length(); i++) { + const DataPair& keyedScalarData = scalarsToReflect[i]; + + // Go through each keyed scalar and create a keyed scalar object. + // This object will hold the values for all the keyed scalar keys. + JS::RootedObject keyedScalarObj(aCx, JS_NewPlainObject(aCx)); + + // Define a property for each scalar key, then add it to the keyed scalar + // object. + const nsTArray<KeyedScalar::KeyValuePair>& keyProps = keyedScalarData.second(); + for (uint32_t i = 0; i < keyProps.Length(); i++) { + const KeyedScalar::KeyValuePair& keyData = keyProps[i]; + + // Convert the value for the key to a JSValue. + JS::Rooted<JS::Value> keyJsValue(aCx); + nsresult rv = + nsContentUtils::XPConnect()->VariantToJS(aCx, keyedScalarObj, keyData.second(), &keyJsValue); + if (NS_FAILED(rv)) { + return rv; + } + + // Add the key to the scalar representation. + const NS_ConvertUTF8toUTF16 key(keyData.first()); + if (!JS_DefineUCProperty(aCx, keyedScalarObj, key.Data(), key.Length(), keyJsValue, JSPROP_ENUMERATE)) { + return NS_ERROR_FAILURE; + } + } + + // Add the scalar to the root object. + if (!JS_DefineProperty(aCx, root_obj, keyedScalarData.first(), keyedScalarObj, JSPROP_ENUMERATE)) { + return NS_ERROR_FAILURE; + } + } + + return NS_OK; +} + +/** + * Resets all the stored scalars. This is intended to be only used in tests. + */ +void +TelemetryScalar::ClearScalars() +{ + StaticMutexAutoLock locker(gTelemetryScalarsMutex); + gScalarStorageMap.Clear(); + gKeyedScalarStorageMap.Clear(); +} + +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; + // For the plain scalars... + for (auto iter = gScalarStorageMap.Iter(); !iter.Done(); iter.Next()) { + ScalarBase* scalar = static_cast<ScalarBase*>(iter.Data()); + n += scalar->SizeOfIncludingThis(aMallocSizeOf); + } + // ...and for the keyed scalars. + for (auto iter = gKeyedScalarStorageMap.Iter(); !iter.Done(); iter.Next()) { + KeyedScalar* scalar = static_cast<KeyedScalar*>(iter.Data()); + n += scalar->SizeOfIncludingThis(aMallocSizeOf); + } + return n; +} |