Source code
Revision control
Copy as Markdown
Other Tools
/* -*- 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
#include "TelemetryScalar.h"
#include "ipc/TelemetryIPCAccumulator.h"
#include "js/Array.h" // JS::GetArrayLength, JS::IsArrayObject
#include "js/PropertyAndElement.h" // JS_DefineProperty, JS_DefineUCProperty, JS_Enumerate, JS_GetElement, JS_GetProperty, JS_GetPropertyById, JS_HasProperty
#include "mozilla/dom/ContentParent.h"
#include "mozilla/JSONWriter.h"
#include "mozilla/Preferences.h"
#include "mozilla/StaticMutex.h"
#include "mozilla/StaticPtr.h"
#include "mozilla/TelemetryComms.h"
#include "mozilla/Unused.h"
#include "nsBaseHashtable.h"
#include "nsClassHashtable.h"
#include "nsContentUtils.h"
#include "nsHashKeys.h"
#include "nsITelemetry.h"
#include "nsIVariant.h"
#include "nsIXPConnect.h"
#include "nsJSUtils.h"
#include "nsPrintfCString.h"
#include "nsVariant.h"
#include "TelemetryCommon.h"
#include "TelemetryScalarData.h"
using mozilla::MakeUnique;
using mozilla::Nothing;
using mozilla::Preferences;
using mozilla::Some;
using mozilla::StaticAutoPtr;
using mozilla::StaticMutex;
using mozilla::StaticMutexAutoLock;
using mozilla::UniquePtr;
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;
namespace geckoprofiler::markers {
struct ScalarMarker {
static constexpr mozilla::Span<const char> MarkerTypeName() {
return mozilla::MakeStringSpan("Scalar");
}
static void StreamJSONMarkerData(
mozilla::baseprofiler::SpliceableJSONWriter& aWriter,
const mozilla::ProfilerString8View& aName, const uint32_t& aKind,
const nsCString& aKey, const ScalarVariant& aValue) {
aWriter.UniqueStringProperty("id", aName);
if (!aKey.IsEmpty()) {
aWriter.StringProperty("key", mozilla::MakeStringSpan(aKey.get()));
}
if (aKind == nsITelemetry::SCALAR_TYPE_COUNT) {
aWriter.UniqueStringProperty("scalarType", "uint");
aWriter.IntProperty("val", aValue.as<uint32_t>());
} else if (aKind == nsITelemetry::SCALAR_TYPE_STRING) {
aWriter.UniqueStringProperty("scalarType", "string");
aWriter.StringProperty(
"val", mozilla::MakeStringSpan(
NS_ConvertUTF16toUTF8(aValue.as<nsString>()).get()));
} else {
aWriter.UniqueStringProperty("scalarType", "bool");
aWriter.BoolProperty("val", aValue.as<bool>());
}
}
using MS = mozilla::MarkerSchema;
static MS MarkerTypeDisplay() {
MS schema{MS::Location::MarkerChart, MS::Location::MarkerTable};
schema.AddKeyLabelFormatSearchable("id", "Scalar Name",
MS::Format::UniqueString,
MS::Searchable::Searchable);
schema.AddKeyLabelFormatSearchable("key", "Key", MS::Format::String,
MS::Searchable::Searchable);
schema.AddKeyLabelFormatSearchable("scalarType", "Type",
MS::Format::UniqueString,
MS::Searchable::Searchable);
schema.AddKeyLabelFormatSearchable("val", "Value", MS::Format::String,
MS::Searchable::Searchable);
schema.SetTooltipLabel(
"{marker.data.id}[{marker.data.key}] {marker.data.val}");
schema.SetTableLabel(
"{marker.name} - {marker.data.id}[{marker.data.key}]: "
"{marker.data.val}");
return schema;
}
};
} // namespace geckoprofiler::markers
////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////
//
// Naming: there are two kinds of functions in this file:
//
// * Functions named internal_*: these can only be reached via an
// interface function (TelemetryScalar::*). If they access shared
// state, they require the interface function to have acquired
// |gTelemetryScalarMutex| to ensure thread safety.
//
// * Functions named TelemetryScalar::*. This is the external interface.
// Entries and exits to these functions are serialised using
// |gTelemetryScalarsMutex|.
//
// Avoiding races and deadlocks:
//
// All functions in the external interface (TelemetryScalar::*) are
// serialised using the mutex |gTelemetryScalarsMutex|. This means
// that the external interface is thread-safe. But it also brings
// a danger of deadlock if any function in the external interface can
// get back to that interface. That is, we will deadlock on any call
// chain like this
//
// TelemetryScalar::* -> .. any functions .. -> TelemetryScalar::*
//
// To reduce the danger of that happening, observe the following rules:
//
// * No function in TelemetryScalar::* may directly call, nor take the
// address of, any other function in TelemetryScalar::*.
//
// * No internal function internal_* may call, nor take the address
// of, any function in TelemetryScalar::*.
////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////
//
// PRIVATE TYPES
namespace {
const uint32_t kMaximumNumberOfKeys = 100;
const uint32_t kMaxEventSummaryKeys = 500;
const uint32_t kMaximumKeyStringLength = 72;
const uint32_t kMaximumStringValueLength = 50;
// The category and scalar name maximum lengths are used by the dynamic
// scalar registration function and must match the constants used by
// the 'parse_scalars.py' script for static scalars.
const uint32_t kMaximumCategoryNameLength = 40;
const uint32_t kMaximumScalarNameLength = 40;
const uint32_t kScalarCount =
static_cast<uint32_t>(mozilla::Telemetry::ScalarID::ScalarCount);
const char* TEST_SCALAR_PREFIX = "telemetry.test.";
// The max offset supported by gScalarStoresTable for static scalars' stores.
// Also the sentinel value (with store_count == 0) for just the sole "main"
// store.
const uint32_t kMaxStaticStoreOffset = UINT16_MAX;
enum class ScalarResult : uint8_t {
// Nothing went wrong.
Ok,
// General Scalar Errors
NotInitialized,
CannotUnpackVariant,
CannotRecordInProcess,
CannotRecordDataset,
KeyedTypeMismatch,
UnknownScalar,
OperationNotSupported,
InvalidType,
InvalidValue,
// Keyed Scalar Errors
KeyIsEmpty,
KeyTooLong,
TooManyKeys,
KeyNotAllowed,
// String Scalar Errors
StringTooLong,
// Unsigned Scalar Errors
UnsignedNegativeValue,
UnsignedTruncatedValue,
};
// A common identifier for both built-in and dynamic scalars.
struct ScalarKey {
uint32_t id;
bool dynamic;
};
// Dynamic scalar store names.
StaticAutoPtr<nsTArray<RefPtr<nsAtom>>> gDynamicStoreNames;
/**
* Scalar information for dynamic definitions.
*/
struct DynamicScalarInfo : BaseScalarInfo {
nsCString mDynamicName;
bool mDynamicExpiration;
uint32_t store_count;
uint32_t store_offset;
DynamicScalarInfo(uint32_t aKind, bool aRecordOnRelease, bool aExpired,
const nsACString& aName, bool aKeyed,
const nsTArray<nsCString>& aStores)
: BaseScalarInfo(
aKind,
aRecordOnRelease ? nsITelemetry::DATASET_ALL_CHANNELS
: nsITelemetry::DATASET_PRERELEASE_CHANNELS,
RecordedProcessType::All, aKeyed, 0, 0, GetCurrentProduct()),
mDynamicName(aName),
mDynamicExpiration(aExpired) {
store_count = aStores.Length();
if (store_count == 0) {
store_count = 1;
store_offset = kMaxStaticStoreOffset;
} else {
store_offset = kMaxStaticStoreOffset + 1 + gDynamicStoreNames->Length();
for (const auto& storeName : aStores) {
gDynamicStoreNames->AppendElement(NS_Atomize(storeName));
}
MOZ_ASSERT(
gDynamicStoreNames->Length() < UINT32_MAX - kMaxStaticStoreOffset - 1,
"Too many dynamic scalar store names. Overflow.");
}
};
// The following functions will read the stored text
// instead of looking it up in the statically generated
// tables.
const char* name() const override;
const char* expiration() const override;
uint32_t storeCount() const override;
uint32_t storeOffset() const override;
};
const char* DynamicScalarInfo::name() const { return mDynamicName.get(); }
const char* DynamicScalarInfo::expiration() const {
// Dynamic scalars can either be expired or not (boolean flag).
// Return an appropriate version string to leverage the scalar expiration
// logic.
return mDynamicExpiration ? "1.0" : "never";
}
uint32_t DynamicScalarInfo::storeOffset() const { return store_offset; }
uint32_t DynamicScalarInfo::storeCount() const { return store_count; }
typedef nsBaseHashtableET<nsDepCharHashKey, ScalarKey> CharPtrEntryType;
typedef AutoHashtable<CharPtrEntryType> ScalarMapType;
// Dynamic scalar definitions.
StaticAutoPtr<nsTArray<DynamicScalarInfo>> gDynamicScalarInfo;
const BaseScalarInfo& internal_GetScalarInfo(const StaticMutexAutoLock& lock,
const ScalarKey& aId) {
if (!aId.dynamic) {
return gScalars[aId.id];
}
return (*gDynamicScalarInfo)[aId.id];
}
bool IsValidEnumId(mozilla::Telemetry::ScalarID aID) {
return aID < mozilla::Telemetry::ScalarID::ScalarCount;
}
bool internal_IsValidId(const StaticMutexAutoLock& lock, const ScalarKey& aId) {
// Please note that this function needs to be called with the scalar
// mutex being acquired: other functions might be messing with
// |gDynamicScalarInfo|.
return aId.dynamic
? (aId.id < gDynamicScalarInfo->Length())
: IsValidEnumId(static_cast<mozilla::Telemetry::ScalarID>(aId.id));
}
// 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;
// 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();
}
// GetValue is used to get the value of the scalar when persisting it to JS.
virtual nsresult GetValue(const nsACString& aStoreName, bool aClearStore,
nsCOMPtr<nsIVariant>& aResult) = 0;
// To measure the memory stats.
size_t SizeOfExcludingThis(mozilla::MallocSizeOf aMallocSizeOf) const;
virtual size_t SizeOfIncludingThis(
mozilla::MallocSizeOf aMallocSizeOf) const = 0;
protected:
bool HasValueInStore(size_t aStoreIndex) const;
void ClearValueInStore(size_t aStoreIndex);
void SetValueInStores();
nsresult StoreIndex(const nsACString& aStoreName, size_t* aStoreIndex) const;
private:
ScalarResult HandleUnsupported() const;
const uint32_t mStoreCount;
const uint32_t mStoreOffset;
nsTArray<bool> mStoreHasValue;
protected:
const nsCString mName;
};
ScalarResult ScalarBase::HandleUnsupported() const {
MOZ_ASSERT(false, "This operation is not support for this scalar type.");
return ScalarResult::OperationNotSupported;
}
bool ScalarBase::HasValueInStore(size_t aStoreIndex) const {
MOZ_ASSERT(aStoreIndex < mStoreHasValue.Length(),
"Invalid scalar store index.");
return mStoreHasValue[aStoreIndex];
}
void ScalarBase::ClearValueInStore(size_t aStoreIndex) {
MOZ_ASSERT(aStoreIndex < mStoreHasValue.Length(),
"Invalid scalar store index to clear.");
mStoreHasValue[aStoreIndex] = false;
}
void ScalarBase::SetValueInStores() {
for (auto& val : mStoreHasValue) {
val = true;
}
}
nsresult ScalarBase::StoreIndex(const nsACString& aStoreName,
size_t* aStoreIndex) const {
if (mStoreCount == 1 && mStoreOffset == kMaxStaticStoreOffset) {
// This Scalar is only in the "main" store.
if (aStoreName.EqualsLiteral("main")) {
*aStoreIndex = 0;
return NS_OK;
}
return NS_ERROR_NO_CONTENT;
}
// Multiple stores. Linear scan to find one that matches aStoreName.
// Dynamic Scalars start at kMaxStaticStoreOffset + 1
if (mStoreOffset > kMaxStaticStoreOffset) {
auto dynamicOffset = mStoreOffset - kMaxStaticStoreOffset - 1;
for (uint32_t i = 0; i < mStoreCount; ++i) {
auto scalarStore = (*gDynamicStoreNames)[dynamicOffset + i];
if (nsAtomCString(scalarStore).Equals(aStoreName)) {
*aStoreIndex = i;
return NS_OK;
}
}
return NS_ERROR_NO_CONTENT;
}
// Static Scalars are similar.
for (uint32_t i = 0; i < mStoreCount; ++i) {
uint32_t stringIndex = gScalarStoresTable[mStoreOffset + i];
if (aStoreName.EqualsASCII(&gScalarsStringTable[stringIndex])) {
*aStoreIndex = i;
return NS_OK;
}
}
return NS_ERROR_NO_CONTENT;
}
size_t ScalarBase::SizeOfExcludingThis(
mozilla::MallocSizeOf aMallocSizeOf) const {
return mStoreHasValue.ShallowSizeOfExcludingThis(aMallocSizeOf);
}
/**
* The implementation for the unsigned int scalar type.
*/
class ScalarUnsigned : public ScalarBase {
public:
using ScalarBase::SetValue;
explicit ScalarUnsigned(const BaseScalarInfo& aInfo)
: ScalarBase(aInfo), mStorage(aInfo.storeCount()) {
mStorage.SetLength(aInfo.storeCount());
for (auto& val : mStorage) {
val = 0;
}
};
~ScalarUnsigned() override = default;
void SetValue(uint32_t aValue) final;
void AddValue(uint32_t aValue) final;
nsresult GetValue(const nsACString& aStoreName, bool aClearStore,
nsCOMPtr<nsIVariant>& aResult) final;
size_t SizeOfIncludingThis(mozilla::MallocSizeOf aMallocSizeOf) const final;
private:
nsTArray<uint32_t> mStorage;
// Prevent copying.
ScalarUnsigned(const ScalarUnsigned& aOther) = delete;
void operator=(const ScalarUnsigned& aOther) = delete;
};
void ScalarUnsigned::SetValue(uint32_t aValue) {
for (auto& val : mStorage) {
val = aValue;
}
SetValueInStores();
}
void ScalarUnsigned::AddValue(uint32_t aValue) {
for (auto& val : mStorage) {
val += aValue;
}
SetValueInStores();
}
nsresult ScalarUnsigned::GetValue(const nsACString& aStoreName,
bool aClearStore,
nsCOMPtr<nsIVariant>& aResult) {
size_t storeIndex = 0;
nsresult rv = StoreIndex(aStoreName, &storeIndex);
if (NS_FAILED(rv)) {
return rv;
}
if (!HasValueInStore(storeIndex)) {
return NS_ERROR_NO_CONTENT;
}
nsCOMPtr<nsIWritableVariant> outVar(new nsVariant());
rv = outVar->SetAsUint32(mStorage[storeIndex]);
if (NS_FAILED(rv)) {
return rv;
}
aResult = std::move(outVar);
if (aClearStore) {
mStorage[storeIndex] = 0;
ClearValueInStore(storeIndex);
}
return NS_OK;
}
size_t ScalarUnsigned::SizeOfIncludingThis(
mozilla::MallocSizeOf aMallocSizeOf) const {
size_t n = aMallocSizeOf(this);
n += ScalarBase::SizeOfExcludingThis(aMallocSizeOf);
n += mStorage.ShallowSizeOfExcludingThis(aMallocSizeOf);
return n;
}
/**
* 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(const nsAString& aValue) final;
nsresult GetValue(const nsACString& aStoreName, bool aClearStore,
nsCOMPtr<nsIVariant>& aResult) final;
size_t SizeOfIncludingThis(mozilla::MallocSizeOf aMallocSizeOf) const final;
private:
nsTArray<nsString> mStorage;
// Prevent copying.
ScalarString(const ScalarString& aOther) = delete;
void operator=(const ScalarString& aOther) = delete;
};
ScalarResult ScalarString::SetValue(const nsAString& aValue) {
auto str = Substring(aValue, 0, kMaximumStringValueLength);
for (auto& val : mStorage) {
val.Assign(str);
}
SetValueInStores();
if (aValue.Length() > kMaximumStringValueLength) {
return ScalarResult::StringTooLong;
}
return ScalarResult::Ok;
}
nsresult ScalarString::GetValue(const nsACString& aStoreName, bool aClearStore,
nsCOMPtr<nsIVariant>& aResult) {
nsCOMPtr<nsIWritableVariant> outVar(new nsVariant());
size_t storeIndex = 0;
nsresult rv = StoreIndex(aStoreName, &storeIndex);
if (NS_FAILED(rv)) {
return rv;
}
if (!HasValueInStore(storeIndex)) {
return NS_ERROR_NO_CONTENT;
}
rv = outVar->SetAsAString(mStorage[storeIndex]);
if (NS_FAILED(rv)) {
return rv;
}
if (aClearStore) {
ClearValueInStore(storeIndex);
}
aResult = std::move(outVar);
return NS_OK;
}
size_t ScalarString::SizeOfIncludingThis(
mozilla::MallocSizeOf aMallocSizeOf) const {
size_t n = aMallocSizeOf(this);
n += ScalarBase::SizeOfExcludingThis(aMallocSizeOf);
n += mStorage.ShallowSizeOfExcludingThis(aMallocSizeOf);
for (auto& val : mStorage) {
n += val.SizeOfExcludingThisIfUnshared(aMallocSizeOf);
}
return n;
}
/**
* The implementation for the boolean scalar type.
*/
class ScalarBoolean : public ScalarBase {
public:
using ScalarBase::SetValue;
explicit ScalarBoolean(const BaseScalarInfo& aInfo)
: ScalarBase(aInfo), mStorage(aInfo.storeCount()) {
mStorage.SetLength(aInfo.storeCount());
for (auto& val : mStorage) {
val = false;
}
};
~ScalarBoolean() override = default;
void SetValue(bool aValue) final;
nsresult GetValue(const nsACString& aStoreName, bool aClearStore,
nsCOMPtr<nsIVariant>& aResult) final;
size_t SizeOfIncludingThis(mozilla::MallocSizeOf aMallocSizeOf) const final;
private:
nsTArray<bool> mStorage;
// Prevent copying.
ScalarBoolean(const ScalarBoolean& aOther) = delete;
void operator=(const ScalarBoolean& aOther) = delete;
};
void ScalarBoolean::SetValue(bool aValue) {
for (auto& val : mStorage) {
val = aValue;
}
SetValueInStores();
}
nsresult ScalarBoolean::GetValue(const nsACString& aStoreName, bool aClearStore,
nsCOMPtr<nsIVariant>& aResult) {
nsCOMPtr<nsIWritableVariant> outVar(new nsVariant());
size_t storeIndex = 0;
nsresult rv = StoreIndex(aStoreName, &storeIndex);
if (NS_FAILED(rv)) {
return rv;
}
if (!HasValueInStore(storeIndex)) {
return NS_ERROR_NO_CONTENT;
}
if (aClearStore) {
ClearValueInStore(storeIndex);
}
rv = outVar->SetAsBool(mStorage[storeIndex]);
if (NS_FAILED(rv)) {
return rv;
}
aResult = std::move(outVar);
return NS_OK;
}
size_t ScalarBoolean::SizeOfIncludingThis(
mozilla::MallocSizeOf aMallocSizeOf) const {
size_t n = aMallocSizeOf(this);
n += ScalarBase::SizeOfExcludingThis(aMallocSizeOf);
n += mStorage.ShallowSizeOfExcludingThis(aMallocSizeOf);
return n;
}
/**
* Allocate a scalar class given the scalar info.
*
* @param aInfo The informations for the scalar coming from the definition file.
* @return nullptr if the scalar type is unknown, otherwise a valid pointer to
* the scalar type.
*/
ScalarBase* internal_ScalarAllocate(const BaseScalarInfo& aInfo) {
ScalarBase* scalar = nullptr;
switch (aInfo.kind) {
case nsITelemetry::SCALAR_TYPE_COUNT:
scalar = new ScalarUnsigned(aInfo);
break;
case nsITelemetry::SCALAR_TYPE_STRING:
scalar = new ScalarString(aInfo);
break;
case nsITelemetry::SCALAR_TYPE_BOOLEAN:
scalar = new ScalarBoolean(aInfo);
break;
default:
MOZ_ASSERT(false, "Invalid scalar type");
}
return scalar;
}
/**
* The implementation for the keyed scalar type.
*/
class KeyedScalar {
public:
typedef std::pair<nsCString, nsCOMPtr<nsIVariant>> KeyValuePair;
// We store the name instead of a reference to the BaseScalarInfo because
// the BaseScalarInfo can move if it's from a dynamic scalar.
explicit KeyedScalar(const BaseScalarInfo& info)
: mScalarName(info.name()),
mScalarKeyCount(info.key_count),
mScalarKeyOffset(info.key_offset),
mMaximumNumberOfKeys(kMaximumNumberOfKeys) {};
~KeyedScalar() = default;
// 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);
// GetValue is used to get the key-value pairs stored in the keyed scalar
// when persisting it to JS.
nsresult GetValue(const nsACString& aStoreName, bool aClearStorage,
nsTArray<KeyValuePair>& aValues);
// To measure the memory stats.
size_t SizeOfIncludingThis(mozilla::MallocSizeOf aMallocSizeOf);
// To permit more keys than normal.
void SetMaximumNumberOfKeys(uint32_t aMaximumNumberOfKeys) {
mMaximumNumberOfKeys = aMaximumNumberOfKeys;
};
private:
typedef nsClassHashtable<nsCStringHashKey, ScalarBase> ScalarKeysMapType;
const nsCString mScalarName;
ScalarKeysMapType mScalarKeys;
uint32_t mScalarKeyCount;
uint32_t mScalarKeyOffset;
uint32_t mMaximumNumberOfKeys;
ScalarResult GetScalarForKey(const StaticMutexAutoLock& locker,
const nsAString& aKey, ScalarBase** aRet);
bool AllowsKey(const nsAString& aKey) const;
};
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) {
// telemetry.keyed_scalars_exceed_limit.
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) {
// telemetry.keyed_scalars_exceed_limit.
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) {
// telemetry.keyed_scalars_exceed_limit.
return;
}
return scalar->AddValue(aValue);
}
/**
* Get a key-value array with the values for the Keyed Scalar.
* @param aValue The array that will hold the key-value pairs.
* @return {nsresult} NS_OK or an error value as reported by the
* the specific scalar objects implementations (e.g.
* ScalarUnsigned).
*/
nsresult KeyedScalar::GetValue(const nsACString& aStoreName, bool aClearStorage,
nsTArray<KeyValuePair>& aValues) {
for (const auto& entry : mScalarKeys) {
ScalarBase* scalar = entry.GetWeak();
// Get the scalar value.
nsCOMPtr<nsIVariant> scalarValue;
nsresult rv = scalar->GetValue(aStoreName, aClearStorage, scalarValue);
if (rv == NS_ERROR_NO_CONTENT) {
// No value for this store.
continue;
}
if (NS_FAILED(rv)) {
return rv;
}
// Append it to value list.
aValues.AppendElement(
std::make_pair(nsCString(entry.GetKey()), scalarValue));
}
return NS_OK;
}
// Forward declaration
nsresult internal_GetKeyedScalarByEnum(const StaticMutexAutoLock& lock,
const ScalarKey& aId,
ProcessID aProcessStorage,
KeyedScalar** aRet);
// Forward declaration
nsresult internal_GetEnumByScalarName(const StaticMutexAutoLock& lock,
const nsACString& aName, ScalarKey* aId);
/**
* Get the scalar for the referenced key.
* If there's no such key, instantiate a new Scalar object with the
* same type of the Keyed scalar and create the key.
*/
ScalarResult KeyedScalar::GetScalarForKey(const StaticMutexAutoLock& locker,
const nsAString& aKey,
ScalarBase** aRet) {
if (aKey.IsEmpty()) {
return ScalarResult::KeyIsEmpty;
}
if (!AllowsKey(aKey)) {
KeyedScalar* scalarUnknown = nullptr;
ScalarKey scalarUnknownUniqueId{
static_cast<uint32_t>(
mozilla::Telemetry::ScalarID::TELEMETRY_KEYED_SCALARS_UNKNOWN_KEYS),
false};
ProcessID process = ProcessID::Parent;
nsresult rv = internal_GetKeyedScalarByEnum(locker, scalarUnknownUniqueId,
process, &scalarUnknown);
if (NS_FAILED(rv)) {
return ScalarResult::TooManyKeys;
}
scalarUnknown->AddValue(locker, NS_ConvertUTF8toUTF16(mScalarName), 1);
return ScalarResult::KeyNotAllowed;
}
if (aKey.Length() > kMaximumKeyStringLength) {
return ScalarResult::KeyTooLong;
}
NS_ConvertUTF16toUTF8 utf8Key(aKey);
ScalarBase* scalar = nullptr;
if (mScalarKeys.Get(utf8Key, &scalar)) {
*aRet = scalar;
return ScalarResult::Ok;
}
ScalarKey uniqueId;
nsresult rv = internal_GetEnumByScalarName(locker, mScalarName, &uniqueId);
if (NS_FAILED(rv)) {
return (rv == NS_ERROR_FAILURE) ? ScalarResult::NotInitialized
: ScalarResult::UnknownScalar;
}
const BaseScalarInfo& info = internal_GetScalarInfo(locker, uniqueId);
if (mScalarKeys.Count() >= mMaximumNumberOfKeys) {
if (aKey.EqualsLiteral("telemetry.keyed_scalars_exceed_limit")) {
return ScalarResult::TooManyKeys;
}
KeyedScalar* scalarExceed = nullptr;
ScalarKey uniqueId{
static_cast<uint32_t>(
mozilla::Telemetry::ScalarID::TELEMETRY_KEYED_SCALARS_EXCEED_LIMIT),
false};
ProcessID process = ProcessID::Parent;
nsresult rv =
internal_GetKeyedScalarByEnum(locker, uniqueId, process, &scalarExceed);
if (NS_FAILED(rv)) {
return ScalarResult::TooManyKeys;
}
scalarExceed->AddValue(locker, NS_ConvertUTF8toUTF16(info.name()), 1);
return ScalarResult::TooManyKeys;
}
scalar = internal_ScalarAllocate(info);
if (!scalar) {
return ScalarResult::InvalidType;
}
mScalarKeys.InsertOrUpdate(utf8Key, UniquePtr<ScalarBase>(scalar));
*aRet = scalar;
return ScalarResult::Ok;
}
size_t KeyedScalar::SizeOfIncludingThis(mozilla::MallocSizeOf aMallocSizeOf) {
size_t n = aMallocSizeOf(this);
for (const auto& scalar : mScalarKeys.Values()) {
n += scalar->SizeOfIncludingThis(aMallocSizeOf);
}
return n;
}
bool KeyedScalar::AllowsKey(const nsAString& aKey) const {
// If we didn't specify a list of allowed keys, just return true.
if (mScalarKeyCount == 0) {
return true;
}
for (uint32_t i = 0; i < mScalarKeyCount; ++i) {
uint32_t stringIndex = gScalarKeysTable[mScalarKeyOffset + i];
if (aKey.EqualsASCII(&gScalarsStringTable[stringIndex])) {
return true;
}
}
return false;
}
typedef nsUint32HashKey ScalarIDHashKey;
typedef nsUint32HashKey ProcessIDHashKey;
typedef nsClassHashtable<ScalarIDHashKey, ScalarBase> ScalarStorageMapType;
typedef nsClassHashtable<ScalarIDHashKey, KeyedScalar>
KeyedScalarStorageMapType;
typedef nsClassHashtable<ProcessIDHashKey, ScalarStorageMapType>
ProcessesScalarsMapType;
typedef nsClassHashtable<ProcessIDHashKey, KeyedScalarStorageMapType>
ProcessesKeyedScalarsMapType;
typedef std::tuple<const char*, nsCOMPtr<nsIVariant>, uint32_t> ScalarDataTuple;
typedef nsTArray<ScalarDataTuple> ScalarTupleArray;
typedef nsTHashMap<ProcessIDHashKey, ScalarTupleArray> ScalarSnapshotTable;
typedef std::tuple<const char*, nsTArray<KeyedScalar::KeyValuePair>, uint32_t>
KeyedScalarDataTuple;
typedef nsTArray<KeyedScalarDataTuple> KeyedScalarTupleArray;
typedef nsTHashMap<ProcessIDHashKey, KeyedScalarTupleArray>
KeyedScalarSnapshotTable;
} // namespace
////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////
//
// PRIVATE STATE, SHARED BY ALL THREADS
namespace {
// Set to true once this global state has been initialized.
bool gInitDone = false;
bool gCanRecordBase;
bool gCanRecordExtended;
// The Name -> ID cache map.
MOZ_RUNINIT 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.
MOZ_RUNINIT ProcessesScalarsMapType gScalarStorageMap;
// As above, for the keyed scalars.
MOZ_RUNINIT ProcessesKeyedScalarsMapType gKeyedScalarStorageMap;
// Provide separate storage for "dynamic builtin" plain and keyed scalars,
// needed to support "build faster" in local developer builds.
MOZ_RUNINIT ProcessesScalarsMapType gDynamicBuiltinScalarStorageMap;
MOZ_RUNINIT ProcessesKeyedScalarsMapType gDynamicBuiltinKeyedScalarStorageMap;
} // 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.
* @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) {
const BaseScalarInfo& info = internal_GetScalarInfo(lock, aId);
PROFILER_MARKER_TEXT(
"ScalarError", TELEMETRY, mozilla::MarkerStack::Capture(),
nsPrintfCString("KeyedTypeMismatch for %s", info.name()));
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)) {
const BaseScalarInfo& info = internal_GetScalarInfo(lock, aId);
PROFILER_MARKER_TEXT(
"ScalarError", TELEMETRY, mozilla::MarkerStack::Capture(),
nsPrintfCString("CannotRecordInProcess for %s", info.name()));
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);
ScalarBase* scalar = nullptr;
// Initialize the scalar storage to the parent storage. This will get
// set to the child storage if needed.
uint32_t storageId = static_cast<uint32_t>(aProcessStorage);
// Put dynamic-builtin scalars (used to support "build faster") in a
// separate storage.
ProcessesScalarsMapType& processStorage =
aId.dynamic ? gDynamicBuiltinScalarStorageMap : gScalarStorageMap;
// Get the process-specific storage or create one if it's not
// available.
ScalarStorageMapType* const scalarStorage =
processStorage.GetOrInsertNew(storageId);
// Check if the scalar is already allocated in the parent or in the child
// storage.
if (scalarStorage->Get(aId.id, &scalar)) {
// Dynamic scalars can expire at any time during the session (e.g. an
// add-on was updated). Check if it expired.
if (aId.dynamic) {
const DynamicScalarInfo& dynInfo =
static_cast<const DynamicScalarInfo&>(info);
if (dynInfo.mDynamicExpiration) {
// The Dynamic scalar is expired.
PROFILER_MARKER_TEXT(
"ScalarError", TELEMETRY, mozilla::MarkerStack::Capture(),
nsPrintfCString("ExpiredDynamicScalar: %s", info.name()));
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())) {
PROFILER_MARKER_TEXT("ScalarError", TELEMETRY,
mozilla::MarkerStack::Capture(),
nsPrintfCString("ExpiredScalar: %s", info.name()));
return NS_ERROR_NOT_AVAILABLE;
}
scalar = internal_ScalarAllocate(info);
if (!scalar) {
return NS_ERROR_INVALID_ARG;
}
scalarStorage->InsertOrUpdate(aId.id, UniquePtr<ScalarBase>(scalar));
*aRet = scalar;
return NS_OK;
}
// For C++ consummers.
static void internal_profilerMarker_impl(
const StaticMutexAutoLock& lock, ScalarActionType aType,
const ScalarVariant& aValue, ScalarKey uniqueId,
const nsAString& aKey = EmptyString()) {
const BaseScalarInfo& info = internal_GetScalarInfo(lock, uniqueId);
PROFILER_MARKER(
aType == ScalarActionType::eSet
? mozilla::ProfilerString8View("Scalar::Set")
: mozilla::ProfilerString8View("Scalar::Add"),
TELEMETRY, {}, ScalarMarker,
mozilla::ProfilerString8View::WrapNullTerminatedString(info.name()),
info.kind, NS_ConvertUTF16toUTF8(aKey), aValue);
}
// For child process data coming from IPCs
static void internal_profilerMarker_impl(
const StaticMutexAutoLock& lock, const ScalarAction& aAction,
const nsCString& aKey = EmptyCString()) {
ScalarKey uniqueId{aAction.mId, aAction.mDynamic};
const BaseScalarInfo& info = internal_GetScalarInfo(lock, uniqueId);
PROFILER_MARKER(
aAction.mActionType == ScalarActionType::eSet
? mozilla::ProfilerString8View("ChildScalar::Set")
: mozilla::ProfilerString8View("ChildScalar::Add"),
TELEMETRY, {}, ScalarMarker,
mozilla::ProfilerString8View::WrapNullTerminatedString(info.name()),
info.kind, aKey, *aAction.mData);
}
#define internal_profilerMarker(...) \
do { \
if (profiler_thread_is_being_profiled_for_markers()) { \
internal_profilerMarker_impl(__VA_ARGS__); \
} \
} while (false)
} // 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);
KeyedScalar* scalar = nullptr;
// Initialize the scalar storage to the parent storage. This will get
// set to the child storage if needed.
uint32_t storageId = static_cast<uint32_t>(aProcessStorage);
// Put dynamic-builtin scalars (used to support "build faster") in a
// separate storage.
ProcessesKeyedScalarsMapType& processStorage =
aId.dynamic ? gDynamicBuiltinKeyedScalarStorageMap
: gKeyedScalarStorageMap;
// Get the process-specific storage or create one if it's not
// available.
KeyedScalarStorageMapType* const scalarStorage =
processStorage.GetOrInsertNew(storageId);
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);
scalarStorage->InsertOrUpdate(aId.id, UniquePtr<KeyedScalar>(scalar));
*aRet = scalar;
return NS_OK;
}
/**
* Helper function to convert an array of |DynamicScalarInfo|
* to |DynamicScalarDefinition| used by the IPC calls.
*/
void internal_DynamicScalarToIPC(
const StaticMutexAutoLock& lock,
const nsTArray<DynamicScalarInfo>& aDynamicScalarInfos,
nsTArray<DynamicScalarDefinition>& aIPCDefs) {
for (auto& info : aDynamicScalarInfos) {
DynamicScalarDefinition stubDefinition;
stubDefinition.type = info.kind;
stubDefinition.dataset = info.dataset;
stubDefinition.expired = info.mDynamicExpiration;
stubDefinition.keyed = info.keyed;
stubDefinition.name = info.mDynamicName;
aIPCDefs.AppendElement(stubDefinition);
}
}
/**
* Broadcasts the dynamic scalar definitions to all the other
* content processes.
*/
void internal_BroadcastDefinitions(
const nsTArray<DynamicScalarDefinition>& scalarDefs) {
nsTArray<mozilla::dom::ContentParent*> parents;
mozilla::dom::ContentParent::GetAll(parents);
if (!parents.Length()) {
return;
}
// Broadcast the definitions to the other content processes.
for (auto parent : parents) {
mozilla::Unused << parent->SendAddDynamicScalars(scalarDefs);
}
}
void internal_RegisterScalars(const StaticMutexAutoLock& lock,
const nsTArray<DynamicScalarInfo>& scalarInfos) {
// Register the new scalars.
if (!gDynamicScalarInfo) {
gDynamicScalarInfo = new nsTArray<DynamicScalarInfo>();
}
if (!gDynamicStoreNames) {
gDynamicStoreNames = new nsTArray<RefPtr<nsAtom>>();
}
for (auto& scalarInfo : scalarInfos) {
// Allow expiring scalars that were already registered.
CharPtrEntryType* existingKey =
gScalarNameIDMap.GetEntry(scalarInfo.name());
if (existingKey) {
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 (const auto& entry : aProcessStorage) {
ScalarStorageMapType* scalarStorage = entry.GetWeak();
ScalarTupleArray& processScalars =
aScalarsToReflect.LookupOrInsert(entry.GetKey());
// Are we in the "Dynamic" process?
bool isDynamicProcess =
ProcessID::Dynamic == static_cast<ProcessID>(entry.GetKey());
// Iterate each available child storage.
for (const auto& childEntry : *scalarStorage) {
ScalarBase* scalar = childEntry.GetWeak();
// Get the informations for this scalar.
const BaseScalarInfo& info = internal_GetScalarInfo(
aLock, ScalarKey{childEntry.GetKey(),
aIsBuiltinDynamic ? true : isDynamicProcess});
// Serialize the scalar if it's in the desired dataset.
if (IsInDataset(info.dataset, aDataset)) {
// Get the scalar value.
nsCOMPtr<nsIVariant> scalarValue;
nsresult rv = scalar->GetValue(aStoreName, aClearScalars, scalarValue);
if (rv == NS_ERROR_NO_CONTENT) {
// No value for this store. Proceed.
continue;
}
if (NS_FAILED(rv)) {
return rv;
}
// Append it to our list.
processScalars.AppendElement(
std::make_tuple(info.name(), scalarValue, info.kind));
}
}
if (processScalars.Length() == 0) {
aScalarsToReflect.Remove(entry.GetKey());
}
}
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 (const auto& entry : aProcessStorage) {
KeyedScalarStorageMapType* scalarStorage = entry.GetWeak();
KeyedScalarTupleArray& processScalars =
aScalarsToReflect.LookupOrInsert(entry.GetKey());
// Are we in the "Dynamic" process?
bool isDynamicProcess =
ProcessID::Dynamic == static_cast<ProcessID>(entry.GetKey());
for (const auto& childEntry : *scalarStorage) {
KeyedScalar* scalar = childEntry.GetWeak();
// Get the informations for this scalar.
const BaseScalarInfo& info = internal_GetScalarInfo(
aLock, ScalarKey{childEntry.GetKey(),
aIsBuiltinDynamic ? true : isDynamicProcess});
// Serialize the scalar if it's in the desired dataset.
if (IsInDataset(info.dataset, aDataset)) {
// Get the keys for this scalar.
nsTArray<KeyedScalar::KeyValuePair> scalarKeyedData;
nsresult rv =
scalar->GetValue(aStoreName, aClearScalars, scalarKeyedData);
if (NS_FAILED(rv)) {
return rv;
}
if (scalarKeyedData.Length() == 0) {
// Don't bother with empty keyed scalars.
continue;
}
// Append it to our list.
processScalars.AppendElement(std::make_tuple(
info.name(), std::move(scalarKeyedData), info.kind));
}
}
if (processScalars.Length() == 0) {
aScalarsToReflect.Remove(entry.GetKey());
}
}
return NS_OK;
}
/**
* Helper function to get a snapshot of the scalars.
*
* @param {aLock} The proof of lock to access scalar data.
* @param {aScalarsToReflect} The table that will contain the snapshot.
* @param {aDataset} The dataset we're asking the snapshot for.
* @param {aClearScalars} Whether or not to clear the scalar storage.
* @param {aStoreName} The name of the store to snapshot.
* @return NS_OK or the error code describing the failure reason.
*/
nsresult internal_GetScalarSnapshot(const StaticMutexAutoLock& aLock,
ScalarSnapshotTable& aScalarsToReflect,
unsigned int aDataset, bool aClearScalars,
const nsACString& aStoreName) {
// Take a snapshot of the scalars.
nsresult rv =
internal_ScalarSnapshotter(aLock, aScalarsToReflect, aDataset,
gScalarStorageMap, false, /*aIsBuiltinDynamic*/
aClearScalars, aStoreName);
if (NS_FAILED(rv)) {
return rv;
}
// And a snapshot of the dynamic builtin ones.
rv = internal_ScalarSnapshotter(aLock, aScalarsToReflect, aDataset,
gDynamicBuiltinScalarStorageMap,
true, /*aIsBuiltinDynamic*/
aClearScalars, aStoreName);
if (NS_FAILED(rv)) {
return rv;
}
return NS_OK;
}
/**
* Helper function to get a snapshot of the keyed scalars.
*
* @param {aLock} The proof of lock to access scalar data.
* @param {aScalarsToReflect} The table that will contain the snapshot.
* @param {aDataset} The dataset we're asking the snapshot for.
* @param {aClearScalars} Whether or not to clear the scalar storage.
* @param {aStoreName} The name of the store to snapshot.
* @return NS_OK or the error code describing the failure reason.
*/
nsresult internal_GetKeyedScalarSnapshot(
const StaticMutexAutoLock& aLock,
KeyedScalarSnapshotTable& aScalarsToReflect, unsigned int aDataset,
bool aClearScalars, const nsACString& aStoreName) {
// Take a snapshot of the scalars.
nsresult rv = internal_KeyedScalarSnapshotter(
aLock, aScalarsToReflect, aDataset, gKeyedScalarStorageMap,
false, /*aIsBuiltinDynamic*/
aClearScalars, aStoreName);
if (NS_FAILED(rv)) {
return rv;
}
// And a snapshot of the dynamic builtin ones.
rv = internal_KeyedScalarSnapshotter(aLock, aScalarsToReflect, aDataset,
gDynamicBuiltinKeyedScalarStorageMap,
true, /*aIsBuiltinDynamic*/
aClearScalars, aStoreName);
if (NS_FAILED(rv)) {
return rv;
}
return NS_OK;
}
} // namespace
// helpers for recording/applying scalar operations
namespace {
void internal_ApplyScalarActions(
const StaticMutexAutoLock& lock,
const nsTArray<mozilla::Telemetry::ScalarAction>& aScalarActions,
const mozilla::Maybe<ProcessID>& aProcessType = Nothing()) {
if (!internal_CanRecordBase(lock)) {
return;
}
for (auto& upd : aScalarActions) {
ScalarKey uniqueId{upd.mId, upd.mDynamic};
if (NS_WARN_IF(!internal_IsValidId(lock, uniqueId))) {
MOZ_ASSERT_UNREACHABLE("Scalar usage requires valid ids.");
continue;
}
if (internal_IsKeyedScalar(lock, uniqueId)) {
continue;
}
// Are we allowed to record this scalar? We don't need to check for
// allowed processes here, that's taken care of when recording
// in child processes.
if (!internal_CanRecordForScalarID(lock, uniqueId)) {
continue;
}
// Either we got passed a process type or it was explicitely set on the
// recorded action. It should never happen that it is set to an invalid
// value (such as ProcessID::Count)
ProcessID processType = aProcessType.valueOr(upd.mProcessType);
MOZ_ASSERT(processType != ProcessID::Count);
// Refresh the data in the parent process with the data coming from the
// child processes.
ScalarBase* scalar = nullptr;
nsresult rv =
internal_GetScalarByEnum(lock, uniqueId, processType, &scalar);
if (NS_FAILED(rv)) {
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;
}
internal_profilerMarker(lock, upd);
// Get the type of this scalar from the scalar ID. We already checked
// for its validity a few lines above.
const uint32_t scalarType = internal_GetScalarInfo(lock, uniqueId).kind;
// Extract the data from the mozilla::Variant.
switch (upd.mActionType) {
case ScalarActionType::eSet: {
switch (scalarType) {
case nsITelemetry::SCALAR_TYPE_COUNT:
if (!upd.mData->is<uint32_t>()) {
NS_WARNING("Attempting to set a count scalar to a non-integer.");
continue;
}
scalar->SetValue(upd.mData->as<uint32_t>());
break;
case nsITelemetry::SCALAR_TYPE_BOOLEAN:
if (!upd.mData->is<bool>()) {
NS_WARNING(
"Attempting to set a boolean scalar to a non-boolean.");
continue;
}
scalar->SetValue(upd.mData->as<bool>());
break;
case nsITelemetry::SCALAR_TYPE_STRING:
if (!upd.mData->is<nsString>()) {
NS_WARNING("Attempting to set a string scalar to a non-string.");
continue;
}
scalar->SetValue(upd.mData->as<nsString>());
break;
}
break;
}
case ScalarActionType::eAdd: {
if (scalarType != nsITelemetry::SCALAR_TYPE_COUNT) {
NS_WARNING("Attempting to add on a non count scalar.");
continue;
}
// We only support adding uint32_t.
if (!upd.mData->is<uint32_t>()) {
NS_WARNING("Attempting to add to a count scalar with a non-integer.");
continue;
}
scalar->AddValue(upd.mData->as<uint32_t>());
break;
}
default:
NS_WARNING("Unsupported action coming from scalar child updates.");
}
}
}
void internal_ApplyKeyedScalarActions(
const StaticMutexAutoLock& lock,
const nsTArray<mozilla::Telemetry::KeyedScalarAction>& aScalarActions,
const mozilla::Maybe<ProcessID>& aProcessType = Nothing()) {
if (!internal_CanRecordBase(lock)) {
return;
}
for (auto& upd : aScalarActions) {
ScalarKey uniqueId{upd.mId, upd.mDynamic};
if (NS_WARN_IF(!internal_IsValidId(lock, uniqueId))) {
MOZ_ASSERT_UNREACHABLE("Scalar usage requires valid ids.");
continue;
}
if (!internal_IsKeyedScalar(lock, uniqueId)) {
continue;
}
// Are we allowed to record this scalar? We don't need to check for
// allowed processes here, that's taken care of when recording
// in child processes.
if (!internal_CanRecordForScalarID(lock, uniqueId)) {
continue;
}
// Either we got passed a process type or it was explicitely set on the
// recorded action. It should never happen that it is set to an invalid
// value (such as ProcessID::Count)
ProcessID processType = aProcessType.valueOr(upd.mProcessType);
MOZ_ASSERT(processType != ProcessID::Count);
// Refresh the data in the parent process with the data coming from the
// child processes.
KeyedScalar* scalar = nullptr;
nsresult rv =
internal_GetKeyedScalarByEnum(lock, uniqueId, processType, &scalar);
if (NS_FAILED(rv)) {
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;
internal_profilerMarker(lock, upd, upd.mKey);
// Extract the data from the mozilla::Variant.
switch (upd.mActionType) {
case ScalarActionType::eSet: {
switch (scalarType) {
case nsITelemetry::SCALAR_TYPE_COUNT:
if (!upd.mData->is<uint32_t>()) {
NS_WARNING("Attempting to set a count scalar to a non-integer.");
continue;
}
scalar->SetValue(lock, NS_ConvertUTF8toUTF16(upd.mKey),
upd.mData->as<uint32_t>());
break;
case nsITelemetry::SCALAR_TYPE_BOOLEAN:
if (!upd.mData->is<bool>()) {
NS_WARNING(
"Attempting to set a boolean scalar to a non-boolean.");
continue;
}
scalar->SetValue(lock, NS_ConvertUTF8toUTF16(upd.mKey),
upd.mData->as<bool>());
break;
default:
NS_WARNING("Unsupported type coming from scalar child updates.");
}
break;
}
case ScalarActionType::eAdd: {
if (scalarType != nsITelemetry::SCALAR_TYPE_COUNT) {
NS_WARNING("Attempting to add on a non count scalar.");
continue;
}
// We only support adding on uint32_t.
if (!upd.mData->is<uint32_t>()) {
NS_WARNING("Attempting to add to a count scalar with a non-integer.");
continue;
}
scalar->AddValue(lock, NS_ConvertUTF8toUTF16(upd.mKey),
upd.mData->as<uint32_t>());
break;
}
default:
NS_WARNING(
"Unsupported action coming from keyed scalar child updates.");
}
}
}
} // 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 MOZ_UNANNOTATED;
void TelemetryScalar::InitializeGlobalState(bool aCanRecordBase,
bool aCanRecordExtended) {
StaticMutexAutoLock locker(gTelemetryScalarsMutex);
MOZ_ASSERT(!gInitDone,
"TelemetryScalar::InitializeGlobalState "
"may only be called once");
gCanRecordBase = aCanRecordBase;
gCanRecordExtended = aCanRecordExtended;
// Populate the static scalar name->id cache. Note that the scalar names are
// statically allocated and come from the automatically generated
// TelemetryScalarData.h.
uint32_t scalarCount =
static_cast<uint32_t>(mozilla::Telemetry::ScalarID::ScalarCount);
for (uint32_t i = 0; i < scalarCount; i++) {
CharPtrEntryType* entry = gScalarNameIDMap.PutEntry(gScalars[i].name());
entry->SetData(ScalarKey{i, false});
}
// To summarize dynamic events we need a dynamic scalar.
const nsTArray<DynamicScalarInfo> initialDynamicScalars({
DynamicScalarInfo{
nsITelemetry::SCALAR_TYPE_COUNT,
true /* recordOnRelease */,
false /* expired */,
nsAutoCString("telemetry.dynamic_event_counts"),
true /* keyed */,
{} /* 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::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 aId The scalar enum id.
* @param aVal The numeric value to add to the scalar.
*/
void TelemetryScalar::Add(mozilla::Telemetry::ScalarID aId, uint32_t aValue) {
if (NS_WARN_IF(!IsValidEnumId(aId))) {
MOZ_ASSERT_UNREACHABLE("Scalar usage requires valid ids.");
return;
}
ScalarKey uniqueId{static_cast<uint32_t>(aId), false};
StaticMutexAutoLock locker(gTelemetryScalarsMutex);
if (internal_CanRecordScalar(locker, uniqueId, false) != ScalarResult::Ok) {
// We can't record this scalar. Bail out.
return;
}
internal_profilerMarker(locker, ScalarActionType::eAdd, ScalarVariant(aValue),
uniqueId);
// Accumulate in the child process if needed.
if (!XRE_IsParentProcess()) {
TelemetryIPCAccumulator::RecordChildScalarAction(
uniqueId.id, uniqueId.dynamic, ScalarActionType::eAdd,
ScalarVariant(aValue));
return;
}
ScalarBase* scalar = nullptr;
nsresult rv =
internal_GetScalarByEnum(locker, uniqueId, ProcessID::Parent, &scalar);
if (NS_FAILED(rv)) {
return;
}
scalar->AddValue(aValue);
}
/**
* Adds the value to the given keyed scalar.
*
* @param aId The scalar enum id.
* @param aKey The key name.
* @param aVal The numeric value to add to the scalar.
*/
void TelemetryScalar::Add(mozilla::Telemetry::ScalarID aId,
const nsAString& aKey, uint32_t aValue) {
if (NS_WARN_IF(!IsValidEnumId(aId))) {
MOZ_ASSERT_UNREACHABLE("Scalar usage requires valid ids.");
return;
}
ScalarKey uniqueId{static_cast<uint32_t>(aId), false};
StaticMutexAutoLock locker(gTelemetryScalarsMutex);
if (internal_CanRecordScalar(locker, uniqueId, true) != ScalarResult::Ok) {
// We can't record this scalar. Bail out.
return;
}
internal_profilerMarker(locker, ScalarActionType::eAdd, ScalarVariant(aValue),
uniqueId, aKey);
// Accumulate in the child process if needed.
if (!XRE_IsParentProcess()) {
TelemetryIPCAccumulator::RecordChildKeyedScalarAction(
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 numeric value.
*
* @param aId The scalar enum id.
* @param aValue The numeric, unsigned value to set the scalar to.
*/
void TelemetryScalar::Set(mozilla::Telemetry::ScalarID aId, uint32_t aValue) {
if (NS_WARN_IF(!IsValidEnumId(aId))) {
MOZ_ASSERT_UNREACHABLE("Scalar usage requires valid ids.");
return;
}
ScalarKey uniqueId{static_cast<uint32_t>(aId), false};
StaticMutexAutoLock locker(gTelemetryScalarsMutex);
if (internal_CanRecordScalar(locker, uniqueId, false) != ScalarResult::Ok) {
// We can't record this scalar. Bail out.
return;
}
internal_profilerMarker(locker, ScalarActionType::eSet, ScalarVariant(aValue),
uniqueId);
// Accumulate in the child process if needed.
if (!XRE_IsParentProcess()) {
TelemetryIPCAccumulator::RecordChildScalarAction(
uniqueId.id, uniqueId.dynamic, ScalarActionType::eSet,
ScalarVariant(aValue));
return;
}
ScalarBase* scalar = nullptr;
nsresult rv =
internal_GetScalarByEnum(locker, uniqueId, ProcessID::Parent, &scalar);
if (NS_FAILED(rv)) {
return;
}
scalar->SetValue(aValue);
}
/**
* Sets the scalar to the given string value.
*
* @param aId The scalar enum id.
* @param aValue The string value to set the scalar to.
*/
void TelemetryScalar::Set(mozilla::Telemetry::ScalarID aId,
const nsAString& aValue) {
if (NS_WARN_IF(!IsValidEnumId(aId))) {
MOZ_ASSERT_UNREACHABLE("Scalar usage requires valid ids.");
return;
}
ScalarKey uniqueId{static_cast<uint32_t>(aId), false};
StaticMutexAutoLock locker(gTelemetryScalarsMutex);
if (internal_CanRecordScalar(locker, uniqueId, false) != ScalarResult::Ok) {
// We can't record this scalar. Bail out.
return;
}
internal_profilerMarker(locker, ScalarActionType::eSet,
ScalarVariant(nsString(aValue)), uniqueId);
// Accumulate in the child process if needed.
if (!XRE_IsParentProcess()) {
TelemetryIPCAccumulator::RecordChildScalarAction(
uniqueId.id, uniqueId.dynamic, ScalarActionType::eSet,
ScalarVariant(nsString(aValue)));
return;
}
ScalarBase* scalar = nullptr;
nsresult rv =
internal_GetScalarByEnum(locker, uniqueId, ProcessID::Parent, &scalar);
if (NS_FAILED(rv)) {
return;
}
scalar->SetValue(aValue);
}
/**
* Sets the scalar to the given boolean value.
*
* @param aId The scalar enum id.
* @param aValue The boolean value to set the scalar to.
*/
void TelemetryScalar::Set(mozilla::Telemetry::ScalarID aId, bool aValue) {
if (NS_WARN_IF(!IsValidEnumId(aId))) {
MOZ_ASSERT_UNREACHABLE("Scalar usage requires valid ids.");
return;
}
ScalarKey uniqueId{static_cast<uint32_t>(aId), false};
StaticMutexAutoLock locker(gTelemetryScalarsMutex);
if (internal_CanRecordScalar(locker, uniqueId, false) != ScalarResult::Ok) {
// We can't record this scalar. Bail out.
return;
}
internal_profilerMarker(locker, ScalarActionType::eSet, ScalarVariant(aValue),
uniqueId);
// Accumulate in the child process if needed.
if (!XRE_IsParentProcess()) {
TelemetryIPCAccumulator::RecordChildScalarAction(
uniqueId.id, uniqueId.dynamic, ScalarActionType::eSet,
ScalarVariant(aValue));
return;
}
ScalarBase* scalar = nullptr;
nsresult rv =
internal_GetScalarByEnum(locker, uniqueId, ProcessID::Parent, &scalar);
if (NS_FAILED(rv)) {
return;
}
scalar->SetValue(aValue);
}
/**
* Sets the keyed scalar to the given numeric value.
*
* @param aId The scalar enum id.
* @param aKey The scalar key.
* @param aValue The numeric, unsigned value to set the scalar to.
*/
void TelemetryScalar::Set(mozilla::Telemetry::ScalarID aId,
const nsAString& aKey, uint32_t aValue) {
if (NS_WARN_IF(!IsValidEnumId(aId))) {
MOZ_ASSERT_UNREACHABLE("Scalar usage requires valid ids.");
return;
}
ScalarKey uniqueId{static_cast<uint32_t>(aId), false};
StaticMutexAutoLock locker(gTelemetryScalarsMutex);
if (internal_CanRecordScalar(locker, uniqueId, true) != ScalarResult::Ok) {
// We can't record this scalar. Bail out.
return;
}
internal_profilerMarker(locker, ScalarActionType::eSet, ScalarVariant(aValue),
uniqueId, aKey);
// Accumulate in the child process if needed.
if (!XRE_IsParentProcess()) {
TelemetryIPCAccumulator::RecordChildKeyedScalarAction(
uniqueId.id, uniqueId.dynamic, aKey, ScalarActionType::eSet,
ScalarVariant(aValue));
return;
}
KeyedScalar* scalar = nullptr;
nsresult rv = internal_GetKeyedScalarByEnum(locker, uniqueId,
ProcessID::Parent, &scalar);
if (NS_FAILED(rv)) {
return;
}
scalar->SetValue(locker, aKey, aValue);
}
/**
* Sets the scalar to the given boolean value.
*
* @param aId The scalar enum id.
* @param aKey The scalar key.
* @param aValue The boolean value to set the scalar to.
*/
void TelemetryScalar::Set(mozilla::Telemetry::ScalarID aId,
const nsAString& aKey, bool aValue) {
if (NS_WARN_IF(!IsValidEnumId(aId))) {
MOZ_ASSERT_UNREACHABLE("Scalar usage requires valid ids.");
return;
}
ScalarKey uniqueId{static_cast<uint32_t>(aId), false};
StaticMutexAutoLock locker(gTelemetryScalarsMutex);
if (internal_CanRecordScalar(locker, uniqueId, true) != ScalarResult::Ok) {
// We can't record this scalar. Bail out.
return;
}
internal_profilerMarker(locker, ScalarActionType::eSet, ScalarVariant(aValue),
uniqueId, aKey);
// Accumulate in the child process if needed.
if (!XRE_IsParentProcess()) {
TelemetryIPCAccumulator::RecordChildKeyedScalarAction(
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);
}
nsresult TelemetryScalar::CreateSnapshots(unsigned int aDataset,
bool aClearScalars, JSContext* aCx,
uint8_t optional_argc,
JS::MutableHandle<JS::Value> aResult,
bool aFilterTest,
const nsACString& aStoreName) {
MOZ_ASSERT(
XRE_IsParentProcess(),
"Snapshotting scalars should only happen in the parent processes.");
// If no arguments were passed in, apply the default value.
if (!optional_argc) {
aClearScalars = false;
}
JS::Rooted<JSObject*> root_obj(aCx, JS_NewPlainObject(aCx));
if (!root_obj) {
return NS_ERROR_FAILURE;
}
aResult.setObject(*root_obj);
// Return `{}` in child processes.
if (!XRE_IsParentProcess()) {
return NS_OK;
}
// Only lock the mutex while accessing our data, without locking any JS
// related code.
ScalarSnapshotTable scalarsToReflect;
{
StaticMutexAutoLock locker(gTelemetryScalarsMutex);
nsresult rv = internal_GetScalarSnapshot(locker, scalarsToReflect, aDataset,
aClearScalars, aStoreName);
if (NS_FAILED(rv)) {
return rv;
}
}
// Reflect it to JS.
for (const auto& entry : scalarsToReflect) {
const ScalarTupleArray& processScalars = entry.GetData();
const char* processName = GetNameForProcessID(ProcessID(entry.GetKey()));
// Create the object that will hold the scalars for this process and add it
// to the returned root object.
JS::Rooted<JSObject*> 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 = std::get<0>(scalar);
if (aFilterTest && strncmp(TEST_SCALAR_PREFIX, scalarName,
strlen(TEST_SCALAR_PREFIX)) == 0) {
continue;
}
// Convert it to a JS Val.
JS::Rooted<JS::Value> scalarJsValue(aCx);
nsresult rv = nsContentUtils::XPConnect()->VariantToJS(
aCx, processObj, std::get<1>(scalar), &scalarJsValue);
if (NS_FAILED(rv)) {
return rv;
}
// Add it to the scalar object.
if (!JS_DefineProperty(aCx, processObj, scalarName, scalarJsValue,
JSPROP_ENUMERATE)) {
return NS_ERROR_FAILURE;
}
}
}
return NS_OK;
}
nsresult TelemetryScalar::CreateKeyedSnapshots(
unsigned int aDataset, bool aClearScalars, JSContext* aCx,
uint8_t optional_argc, JS::MutableHandle<JS::Value> aResult,
bool aFilterTest, const nsACString& aStoreName) {
MOZ_ASSERT(
XRE_IsParentProcess(),
"Snapshotting scalars should only happen in the parent processes.");
// If no arguments were passed in, apply the default value.
if (!optional_argc) {
aClearScalars = false;
}
JS::Rooted<JSObject*> root_obj(aCx, JS_NewPlainObject(aCx));
if (!root_obj) {
return NS_ERROR_FAILURE;
}
aResult.setObject(*root_obj);
// Return `{}` in child processes.
if (!XRE_IsParentProcess()) {
return NS_OK;
}
// Only lock the mutex while accessing our data, without locking any JS
// related code.
KeyedScalarSnapshotTable scalarsToReflect;
{
StaticMutexAutoLock locker(gTelemetryScalarsMutex);
nsresult rv = internal_GetKeyedScalarSnapshot(
locker, scalarsToReflect, aDataset, aClearScalars, aStoreName);
if (NS_FAILED(rv)) {
return rv;
}
}
// Reflect it to JS.
for (const auto& entry : scalarsToReflect) {
const KeyedScalarTupleArray& processScalars = entry.GetData();
const char* processName = GetNameForProcessID(ProcessID(entry.GetKey()));
// Create the object that will hold the scalars for this process and add it
// to the returned root object.
JS::Rooted<JSObject*> 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 = std::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::Rooted<JSObject*> 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 =
std::get<1>(keyedScalarData);
for (uint32_t i = 0; i < keyProps.Length(); i++) {
const KeyedScalar::KeyValuePair& keyData = keyProps[i];
// Convert the value for the key to a JSValue.
JS::Rooted<JS::Value> keyJsValue(aCx);
nsresult rv = nsContentUtils::XPConnect()->VariantToJS(
aCx, keyedScalarObj, keyData.second, &keyJsValue);
if (NS_FAILED(rv)) {
return rv;
}
// Add the key to the scalar representation.
const NS_ConvertUTF8toUTF16 key(keyData.first);
if (!JS_DefineUCProperty(aCx, keyedScalarObj, key.Data(), key.Length(),
keyJsValue, JSPROP_ENUMERATE)) {
return NS_ERROR_FAILURE;
}
}
// Add the scalar to the root object.
if (!JS_DefineProperty(aCx, processObj, scalarName, keyedScalarObj,
JSPROP_ENUMERATE)) {
return NS_ERROR_FAILURE;
}
}
}
return NS_OK;
}
nsresult TelemetryScalar::RegisterScalars(const nsACString& aCategoryName,
JS::Handle<JS::Value> aScalarData,
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::Rooted<JSObject*> obj(cx, &aScalarData.toObject());
JS::Rooted<JS::IdVector> scalarPropertyIds(cx, JS::IdVector(cx));
if (!JS_Enumerate(cx, obj, &scalarPropertyIds)) {
return NS_ERROR_FAILURE;
}
// Collect the scalar data into local storage first.
// Only after successfully validating all contained scalars will we register
// them into global storage.
nsTArray<DynamicScalarInfo> newScalarInfos;
for (size_t i = 0, n = scalarPropertyIds.length(); i < n; i++) {
nsAutoJSString scalarName;
if (!scalarName.init(cx, scalarPropertyIds[i])) {
return NS_ERROR_FAILURE;
}
if (!IsValidIdentifierString(NS_ConvertUTF16toUTF8(scalarName),
kMaximumScalarNameLength, false, true)) {
JS_ReportErrorASCII(
cx, "Invalid scalar name %s.",
PromiseFlatCString(NS_ConvertUTF16toUTF8(scalarName)).get());
return NS_ERROR_INVALID_ARG;
}
// Join the category and the probe names.
nsPrintfCString fullName("%s.%s", PromiseFlatCString(aCategoryName).get(),
NS_ConvertUTF16toUTF8(scalarName).get());
JS::Rooted<JS::Value> value(cx);
if (!JS_GetPropertyById(cx, obj, scalarPropertyIds[i], &value) ||
!value.isObject()) {
return NS_ERROR_FAILURE;
}
JS::Rooted<JSObject*> scalarDef(cx, &value.toObject());
// Get the scalar's kind.
if (!JS_GetProperty(cx, scalarDef, "kind", &value) || !value.isInt32()) {
JS_ReportErrorASCII(cx, "Invalid or missing 'kind' for scalar %s.",
PromiseFlatCString(fullName).get());
return NS_ERROR_FAILURE;
}
uint32_t kind = static_cast<uint32_t>(value.toInt32());
// Get the optional scalar's recording policy (default to false).
bool hasProperty = false;
bool recordOnRelease = false;
if (JS_HasProperty(cx, scalarDef, "record_on_release", &hasProperty) &&
hasProperty) {
if (!JS_GetProperty(cx, scalarDef, "record_on_release", &value) ||
!value.isBoolean()) {
JS_ReportErrorASCII(cx, "Invalid 'record_on_release' for scalar %s.",
PromiseFlatCString(fullName).get());
return NS_ERROR_FAILURE;
}
recordOnRelease = static_cast<bool>(value.toBoolean());
}
// Get the optional scalar's keyed (default to false).
bool keyed = false;
if (JS_HasProperty(cx, scalarDef, "keyed", &hasProperty) && hasProperty) {
if (!JS_GetProperty(cx, scalarDef, "keyed", &value) ||
!value.isBoolean()) {
JS_ReportErrorASCII(cx, "Invalid 'keyed' for scalar %s.",
PromiseFlatCString(fullName).get());
return NS_ERROR_FAILURE;
}
keyed = static_cast<bool>(value.toBoolean());
}
// Get the optional scalar's expired state (default to false).
bool expired = false;
if (JS_HasProperty(cx, scalarDef, "expired", &hasProperty) && hasProperty) {
if (!JS_GetProperty(cx, scalarDef, "expired", &value) ||
!value.isBoolean()) {
JS_ReportErrorASCII(cx, "Invalid 'expired' for scalar %s.",
PromiseFlatCString(fullName).get());
return NS_ERROR_FAILURE;
}
expired = static_cast<bool>(value.toBoolean());
}
// Get the scalar's optional stores list (default to ["main"]).
nsTArray<nsCString> stores;
if (JS_HasProperty(cx, scalarDef, "stores", &hasProperty) && hasProperty) {
bool isArray = false;
if (!JS_GetProperty(cx, scalarDef, "stores", &value) ||
!JS::IsArrayObject(cx, value, &isArray) || !isArray) {
JS_ReportErrorASCII(cx, "Invalid 'stores' for scalar %s.",
PromiseFlatCString(fullName).get());
return NS_ERROR_FAILURE;
}
JS::Rooted<JSObject*> arrayObj(cx, &value.toObject());
uint32_t storesLength = 0;
if (!JS::GetArrayLength(cx, arrayObj, &storesLength)) {
JS_ReportErrorASCII(cx,
"Can't get 'stores' array length for scalar %s.",
PromiseFlatCString(fullName).get());
return NS_ERROR_FAILURE;
}
for (uint32_t i = 0; i < storesLength; ++i) {
JS::Rooted<JS::Value> elt(cx);
if (!JS_GetElement(cx, arrayObj, i, &elt)) {
JS_ReportErrorASCII(
cx, "Can't get element from scalar %s 'stores' array.",
PromiseFlatCString(fullName).get());
return NS_ERROR_FAILURE;
}
if (!elt.isString()) {
JS_ReportErrorASCII(cx,
"Element in scalar %s 'stores' array isn't a "
"string.",
PromiseFlatCString(fullName).get());
return NS_ERROR_FAILURE;
}
nsAutoJSString jsStr;
if (!jsStr.init(cx, elt)) {
return NS_ERROR_FAILURE;
}
stores.AppendElement(NS_ConvertUTF16toUTF8(jsStr));
}
// In the event of the usual case (just "main"), save the storage.
if (stores.Length() == 1 && stores[0].EqualsLiteral("main")) {
stores.TruncateLength(0);
}
}
// We defer the actual registration here in case any other event description
// is invalid. In that case we don't need to roll back any partial
// registration.
newScalarInfos.AppendElement(DynamicScalarInfo{
kind, recordOnRelease, expired, fullName, keyed, std::move(stores)});
}
// Register the dynamic definition on the parent process.
nsTArray<DynamicScalarDefinition> ipcDefinitions;
{
StaticMutexAutoLock locker(gTelemetryScalarsMutex);
::internal_RegisterScalars(locker, newScalarInfos);
// Convert the internal scalar representation to a stripped down IPC one.
::internal_DynamicScalarToIPC(locker, newScalarInfos, ipcDefinitions);
}
// Propagate the registration to all the content-processes.
// Do not hold the mutex while calling IPC.
::internal_BroadcastDefinitions(ipcDefinitions);
return NS_OK;
}
/**
*
* 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
*/
void TelemetryScalar::SummarizeEvent(const nsCString& aUniqueEventName,
ProcessID aProcessType) {
MOZ_ASSERT(XRE_IsParentProcess(),
"Only summarize events in the parent process");
if (!XRE_IsParentProcess()) {
return;
}
StaticMutexAutoLock lock(gTelemetryScalarsMutex);
ScalarKey scalarKey{static_cast<uint32_t>(ScalarID::TELEMETRY_EVENT_COUNTS)};
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();
}
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 (const auto& scalarStorage : storageMap.Values()) {
for (const auto& scalar : scalarStorage->Values()) {
partial += scalar->SizeOfIncludingThis(aMallocSizeOf);
}
}
return partial;
};
// Account for all the storage used for the different scalar types.
n += getSizeOf(gScalarStorageMap);
n += getSizeOf(gKeyedScalarStorageMap);
n += getSizeOf(gDynamicBuiltinScalarStorageMap);
n += getSizeOf(gDynamicBuiltinKeyedScalarStorageMap);
return n;
}
void TelemetryScalar::UpdateChildData(
ProcessID aProcessType,
const nsTArray<mozilla::Telemetry::ScalarAction>& aScalarActions) {
MOZ_ASSERT(XRE_IsParentProcess(),
"The stored child processes scalar data must be updated from the "
"parent process.");
StaticMutexAutoLock locker(gTelemetryScalarsMutex);
internal_ApplyScalarActions(locker, aScalarActions, Some(aProcessType));
}
void TelemetryScalar::UpdateChildKeyedData(
ProcessID aProcessType,
const nsTArray<mozilla::Telemetry::KeyedScalarAction>& aScalarActions) {
MOZ_ASSERT(XRE_IsParentProcess(),
"The stored child processes keyed scalar data must be updated "
"from the parent process.");
StaticMutexAutoLock locker(gTelemetryScalarsMutex);
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;
}
ScalarBase* scalar = nullptr;
mozilla::DebugOnly<nsresult> rv;
#define REPORT_DISCARDED(id, member) \
if (aDiscardedData.member) { \
ScalarKey uniqueId = \
ScalarKey{static_cast<uint32_t>(ScalarID::id), false}; \
rv = internal_GetScalarByEnum(locker, uniqueId, aProcessType, &scalar); \
MOZ_ASSERT(NS_SUCCEEDED(rv)); \
internal_profilerMarker(locker, ScalarActionType::eAdd, \
ScalarVariant(aDiscardedData.member), uniqueId); \
scalar->AddValue(aDiscardedData.member); \
}
REPORT_DISCARDED(TELEMETRY_DISCARDED_ACCUMULATIONS,
mDiscardedHistogramAccumulations)
REPORT_DISCARDED(TELEMETRY_DISCARDED_KEYED_ACCUMULATIONS,
mDiscardedKeyedHistogramAccumulations)
REPORT_DISCARDED(TELEMETRY_DISCARDED_SCALAR_ACTIONS, mDiscardedScalarActions)
REPORT_DISCARDED(TELEMETRY_DISCARDED_KEYED_SCALAR_ACTIONS,
mDiscardedKeyedScalarActions)
REPORT_DISCARDED(TELEMETRY_DISCARDED_CHILD_EVENTS, mDiscardedChildEvents)
}
/**
* Get the dynamic scalar definitions in an IPC-friendly
* structure.
*/
void TelemetryScalar::GetDynamicScalarDefinitions(
nsTArray<DynamicScalarDefinition>& aDefArray) {
MOZ_ASSERT(XRE_IsParentProcess());
if (!gDynamicScalarInfo) {
// Don't have dynamic scalar definitions. Bail out!
return;
}
StaticMutexAutoLock locker(gTelemetryScalarsMutex);
internal_DynamicScalarToIPC(locker, *gDynamicScalarInfo, aDefArray);
}
/**
* This adds the dynamic scalar definitions coming from
* the parent process to this child process. If a dynamic
* scalar definition is already defined, check if the new definition
* makes the scalar expired and eventually update the expiration
* state.
*/
void TelemetryScalar::AddDynamicScalarDefinitions(
const nsTArray<DynamicScalarDefinition>& aDefs) {
MOZ_ASSERT(!XRE_IsParentProcess());
nsTArray<DynamicScalarInfo> dynamicStubs;
// Populate the definitions array before acquiring the lock.
for (auto& def : aDefs) {
bool recordOnRelease = def.dataset == nsITelemetry::DATASET_ALL_CHANNELS;
dynamicStubs.AppendElement(DynamicScalarInfo{def.type,
recordOnRelease,
def.expired,
def.name,
def.keyed,
{} /* 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.Insert(store, mozilla::fallible)) {
return NS_ERROR_FAILURE;
}
}
// Dynamic stores
for (auto& ptr : *gDynamicStoreNames) {
nsAutoCString store;
ptr->ToUTF8String(store);
if (!set.Insert(store, mozilla::fallible)) {
return NS_ERROR_FAILURE;
}
}
return NS_OK;
}