summaryrefslogtreecommitdiffstats
path: root/toolkit/components/telemetry/core/TelemetryEvent.cpp
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 19:33:14 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 19:33:14 +0000
commit36d22d82aa202bb199967e9512281e9a53db42c9 (patch)
tree105e8c98ddea1c1e4784a60a5a6410fa416be2de /toolkit/components/telemetry/core/TelemetryEvent.cpp
parentInitial commit. (diff)
downloadfirefox-esr-36d22d82aa202bb199967e9512281e9a53db42c9.tar.xz
firefox-esr-36d22d82aa202bb199967e9512281e9a53db42c9.zip
Adding upstream version 115.7.0esr.upstream/115.7.0esr
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'toolkit/components/telemetry/core/TelemetryEvent.cpp')
-rw-r--r--toolkit/components/telemetry/core/TelemetryEvent.cpp1387
1 files changed, 1387 insertions, 0 deletions
diff --git a/toolkit/components/telemetry/core/TelemetryEvent.cpp b/toolkit/components/telemetry/core/TelemetryEvent.cpp
new file mode 100644
index 0000000000..ad6a3fd2c7
--- /dev/null
+++ b/toolkit/components/telemetry/core/TelemetryEvent.cpp
@@ -0,0 +1,1387 @@
+/* -*- 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 "Telemetry.h"
+#include "TelemetryEvent.h"
+#include <limits>
+#include "ipc/TelemetryIPCAccumulator.h"
+#include "jsapi.h"
+#include "js/Array.h" // JS::GetArrayLength, JS::IsArrayObject, JS::NewArrayObject
+#include "js/PropertyAndElement.h" // JS_DefineElement, JS_DefineProperty, JS_Enumerate, JS_GetElement, JS_GetProperty, JS_GetPropertyById, JS_HasProperty
+#include "mozilla/Maybe.h"
+#include "mozilla/Services.h"
+#include "mozilla/StaticMutex.h"
+#include "mozilla/StaticPtr.h"
+#include "nsClassHashtable.h"
+#include "nsHashKeys.h"
+#include "nsIObserverService.h"
+#include "nsITelemetry.h"
+#include "nsJSUtils.h"
+#include "nsPrintfCString.h"
+#include "nsTArray.h"
+#include "nsUTF8Utils.h"
+#include "nsXULAppAPI.h"
+#include "TelemetryCommon.h"
+#include "TelemetryEventData.h"
+#include "TelemetryScalar.h"
+
+using mozilla::MakeUnique;
+using mozilla::Maybe;
+using mozilla::StaticAutoPtr;
+using mozilla::StaticMutex;
+using mozilla::StaticMutexAutoLock;
+using mozilla::TimeStamp;
+using mozilla::UniquePtr;
+using mozilla::Telemetry::ChildEventData;
+using mozilla::Telemetry::EventExtraEntry;
+using mozilla::Telemetry::LABELS_TELEMETRY_EVENT_RECORDING_ERROR;
+using mozilla::Telemetry::LABELS_TELEMETRY_EVENT_REGISTRATION_ERROR;
+using mozilla::Telemetry::ProcessID;
+using mozilla::Telemetry::Common::CanRecordDataset;
+using mozilla::Telemetry::Common::CanRecordInProcess;
+using mozilla::Telemetry::Common::CanRecordProduct;
+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::MsSinceProcessStart;
+using mozilla::Telemetry::Common::ToJSString;
+
+namespace TelemetryIPCAccumulator = mozilla::TelemetryIPCAccumulator;
+
+////////////////////////////////////////////////////////////////////////
+////////////////////////////////////////////////////////////////////////
+//
+// Naming: there are two kinds of functions in this file:
+//
+// * Functions taking a StaticMutexAutoLock: these can only be reached via
+// an interface function (TelemetryEvent::*). They expect the interface
+// function to have acquired |gTelemetryEventsMutex|, so they do not
+// have to be thread-safe.
+//
+// * Functions named TelemetryEvent::*. This is the external interface.
+// Entries and exits to these functions are serialised using
+// |gTelemetryEventsMutex|.
+//
+// Avoiding races and deadlocks:
+//
+// All functions in the external interface (TelemetryEvent::*) are
+// serialised using the mutex |gTelemetryEventsMutex|. This means
+// that the external interface is thread-safe, and 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:
+//
+// TelemetryEvent::* -> .. any functions .. -> TelemetryEvent::*
+//
+// To reduce the danger of that happening, observe the following rules:
+//
+// * No function in TelemetryEvent::* may directly call, nor take the
+// address of, any other function in TelemetryEvent::*.
+//
+// * No internal function may call, nor take the address
+// of, any function in TelemetryEvent::*.
+
+////////////////////////////////////////////////////////////////////////
+////////////////////////////////////////////////////////////////////////
+//
+// PRIVATE TYPES
+
+namespace {
+
+const uint32_t kEventCount =
+ static_cast<uint32_t>(mozilla::Telemetry::EventID::EventCount);
+// This is a special event id used to mark expired events, to make expiry checks
+// cheap at runtime.
+const uint32_t kExpiredEventId = std::numeric_limits<uint32_t>::max();
+static_assert(kExpiredEventId > kEventCount,
+ "Built-in event count should be less than the expired event id.");
+
+// Maximum length of any passed value string, in UTF8 byte sequence length.
+const uint32_t kMaxValueByteLength = 80;
+// Maximum length of any string value in the extra dictionary, in UTF8 byte
+// sequence length.
+const uint32_t kMaxExtraValueByteLength = 80;
+// Maximum length of dynamic method names, in UTF8 byte sequence length.
+const uint32_t kMaxMethodNameByteLength = 20;
+// Maximum length of dynamic object names, in UTF8 byte sequence length.
+const uint32_t kMaxObjectNameByteLength = 20;
+// Maximum length of extra key names, in UTF8 byte sequence length.
+const uint32_t kMaxExtraKeyNameByteLength = 15;
+// The maximum number of valid extra keys for an event.
+const uint32_t kMaxExtraKeyCount = 10;
+// The number of event records allowed in an event ping.
+const uint32_t kEventPingLimit = 1000;
+
+struct EventKey {
+ uint32_t id;
+ bool dynamic;
+
+ EventKey() : id(kExpiredEventId), dynamic(false) {}
+ EventKey(uint32_t id_, bool dynamic_) : id(id_), dynamic(dynamic_) {}
+};
+
+struct DynamicEventInfo {
+ DynamicEventInfo(const nsACString& category, const nsACString& method,
+ const nsACString& object, nsTArray<nsCString>& extra_keys,
+ bool recordOnRelease, bool builtin)
+ : category(category),
+ method(method),
+ object(object),
+ extra_keys(extra_keys.Clone()),
+ recordOnRelease(recordOnRelease),
+ builtin(builtin) {}
+
+ DynamicEventInfo(const DynamicEventInfo&) = default;
+ DynamicEventInfo& operator=(const DynamicEventInfo&) = delete;
+
+ const nsCString category;
+ const nsCString method;
+ const nsCString object;
+ const CopyableTArray<nsCString> extra_keys;
+ const bool recordOnRelease;
+ const bool builtin;
+
+ size_t SizeOfExcludingThis(mozilla::MallocSizeOf aMallocSizeOf) const {
+ size_t n = 0;
+
+ n += category.SizeOfExcludingThisIfUnshared(aMallocSizeOf);
+ n += method.SizeOfExcludingThisIfUnshared(aMallocSizeOf);
+ n += object.SizeOfExcludingThisIfUnshared(aMallocSizeOf);
+ n += extra_keys.ShallowSizeOfExcludingThis(aMallocSizeOf);
+ for (auto& key : extra_keys) {
+ n += key.SizeOfExcludingThisIfUnshared(aMallocSizeOf);
+ }
+
+ return n;
+ }
+};
+
+enum class RecordEventResult {
+ Ok,
+ UnknownEvent,
+ InvalidExtraKey,
+ StorageLimitReached,
+ ExpiredEvent,
+ WrongProcess,
+ CannotRecord,
+};
+
+typedef CopyableTArray<EventExtraEntry> ExtraArray;
+
+class EventRecord {
+ public:
+ EventRecord(double timestamp, const EventKey& key,
+ const Maybe<nsCString>& value, const ExtraArray& extra)
+ : mTimestamp(timestamp),
+ mEventKey(key),
+ mValue(value),
+ mExtra(extra.Clone()) {}
+
+ EventRecord(const EventRecord& other) = default;
+
+ EventRecord& operator=(const EventRecord& other) = delete;
+
+ double Timestamp() const { return mTimestamp; }
+ const EventKey& GetEventKey() const { return mEventKey; }
+ const Maybe<nsCString>& Value() const { return mValue; }
+ const ExtraArray& Extra() const { return mExtra; }
+
+ size_t SizeOfExcludingThis(mozilla::MallocSizeOf aMallocSizeOf) const;
+
+ private:
+ const double mTimestamp;
+ const EventKey mEventKey;
+ const Maybe<nsCString> mValue;
+ const ExtraArray mExtra;
+};
+
+// Implements the methods for EventInfo.
+const nsDependentCString EventInfo::method() const {
+ return nsDependentCString(&gEventsStringTable[this->method_offset]);
+}
+
+const nsDependentCString EventInfo::object() const {
+ return nsDependentCString(&gEventsStringTable[this->object_offset]);
+}
+
+// Implements the methods for CommonEventInfo.
+const nsDependentCString CommonEventInfo::category() const {
+ return nsDependentCString(&gEventsStringTable[this->category_offset]);
+}
+
+const nsDependentCString CommonEventInfo::expiration_version() const {
+ return nsDependentCString(
+ &gEventsStringTable[this->expiration_version_offset]);
+}
+
+const nsDependentCString CommonEventInfo::extra_key(uint32_t index) const {
+ MOZ_ASSERT(index < this->extra_count);
+ uint32_t key_index = gExtraKeysTable[this->extra_index + index];
+ return nsDependentCString(&gEventsStringTable[key_index]);
+}
+
+// Implementation for the EventRecord class.
+size_t EventRecord::SizeOfExcludingThis(
+ mozilla::MallocSizeOf aMallocSizeOf) const {
+ size_t n = 0;
+
+ if (mValue) {
+ n += mValue.value().SizeOfExcludingThisIfUnshared(aMallocSizeOf);
+ }
+
+ n += mExtra.ShallowSizeOfExcludingThis(aMallocSizeOf);
+ for (uint32_t i = 0; i < mExtra.Length(); ++i) {
+ n += mExtra[i].key.SizeOfExcludingThisIfUnshared(aMallocSizeOf);
+ n += mExtra[i].value.SizeOfExcludingThisIfUnshared(aMallocSizeOf);
+ }
+
+ return n;
+}
+
+nsCString UniqueEventName(const nsACString& category, const nsACString& method,
+ const nsACString& object) {
+ nsCString name;
+ name.Append(category);
+ name.AppendLiteral("#");
+ name.Append(method);
+ name.AppendLiteral("#");
+ name.Append(object);
+ return name;
+}
+
+nsCString UniqueEventName(const EventInfo& info) {
+ return UniqueEventName(info.common_info.category(), info.method(),
+ info.object());
+}
+
+nsCString UniqueEventName(const DynamicEventInfo& info) {
+ return UniqueEventName(info.category, info.method, info.object);
+}
+
+void TruncateToByteLength(nsCString& str, uint32_t length) {
+ // last will be the index of the first byte of the current multi-byte
+ // sequence.
+ uint32_t last = RewindToPriorUTF8Codepoint(str.get(), length);
+ str.Truncate(last);
+}
+
+} // anonymous 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 EventName -> EventKey cache map.
+nsTHashMap<nsCStringHashKey, EventKey> gEventNameIDMap(kEventCount);
+
+// The CategoryName set.
+nsTHashSet<nsCString> gCategoryNames;
+
+// This tracks the IDs of the categories for which recording is enabled.
+nsTHashSet<nsCString> gEnabledCategories;
+
+// The main event storage. Events are inserted here, keyed by process id and
+// in recording order.
+typedef nsUint32HashKey ProcessIDHashKey;
+typedef nsTArray<EventRecord> EventRecordArray;
+typedef nsClassHashtable<ProcessIDHashKey, EventRecordArray>
+ EventRecordsMapType;
+
+EventRecordsMapType gEventRecords;
+
+// The details on dynamic events that are recorded from addons are registered
+// here.
+StaticAutoPtr<nsTArray<DynamicEventInfo>> gDynamicEventInfo;
+
+} // namespace
+
+////////////////////////////////////////////////////////////////////////
+////////////////////////////////////////////////////////////////////////
+//
+// PRIVATE: thread-safe helpers for event recording.
+
+namespace {
+
+unsigned int GetDataset(const StaticMutexAutoLock& lock,
+ const EventKey& eventKey) {
+ if (!eventKey.dynamic) {
+ return gEventInfo[eventKey.id].common_info.dataset;
+ }
+
+ if (!gDynamicEventInfo) {
+ return nsITelemetry::DATASET_PRERELEASE_CHANNELS;
+ }
+
+ return (*gDynamicEventInfo)[eventKey.id].recordOnRelease
+ ? nsITelemetry::DATASET_ALL_CHANNELS
+ : nsITelemetry::DATASET_PRERELEASE_CHANNELS;
+}
+
+nsCString GetCategory(const StaticMutexAutoLock& lock,
+ const EventKey& eventKey) {
+ if (!eventKey.dynamic) {
+ return gEventInfo[eventKey.id].common_info.category();
+ }
+
+ if (!gDynamicEventInfo) {
+ return ""_ns;
+ }
+
+ return (*gDynamicEventInfo)[eventKey.id].category;
+}
+
+bool CanRecordEvent(const StaticMutexAutoLock& lock, const EventKey& eventKey,
+ ProcessID process) {
+ if (!gCanRecordBase) {
+ return false;
+ }
+
+ if (!CanRecordDataset(GetDataset(lock, eventKey), gCanRecordBase,
+ gCanRecordExtended)) {
+ return false;
+ }
+
+ // We don't allow specifying a process to record in for dynamic events.
+ if (!eventKey.dynamic) {
+ const CommonEventInfo& info = gEventInfo[eventKey.id].common_info;
+
+ if (!CanRecordProduct(info.products)) {
+ return false;
+ }
+
+ if (!CanRecordInProcess(info.record_in_processes, process)) {
+ return false;
+ }
+ }
+
+ return true;
+}
+
+bool IsExpired(const EventKey& key) { return key.id == kExpiredEventId; }
+
+EventRecordArray* GetEventRecordsForProcess(const StaticMutexAutoLock& lock,
+ ProcessID processType) {
+ return gEventRecords.GetOrInsertNew(uint32_t(processType));
+}
+
+bool GetEventKey(const StaticMutexAutoLock& lock, const nsACString& category,
+ const nsACString& method, const nsACString& object,
+ EventKey* aEventKey) {
+ const nsCString& name = UniqueEventName(category, method, object);
+ return gEventNameIDMap.Get(name, aEventKey);
+}
+
+static bool CheckExtraKeysValid(const EventKey& eventKey,
+ const ExtraArray& extra) {
+ nsTHashSet<nsCString> validExtraKeys;
+ if (!eventKey.dynamic) {
+ const CommonEventInfo& common = gEventInfo[eventKey.id].common_info;
+ for (uint32_t i = 0; i < common.extra_count; ++i) {
+ validExtraKeys.Insert(common.extra_key(i));
+ }
+ } else if (gDynamicEventInfo) {
+ const DynamicEventInfo& info = (*gDynamicEventInfo)[eventKey.id];
+ for (uint32_t i = 0, len = info.extra_keys.Length(); i < len; ++i) {
+ validExtraKeys.Insert(info.extra_keys[i]);
+ }
+ }
+
+ for (uint32_t i = 0; i < extra.Length(); ++i) {
+ if (!validExtraKeys.Contains(extra[i].key)) {
+ return false;
+ }
+ }
+
+ return true;
+}
+
+RecordEventResult RecordEvent(const StaticMutexAutoLock& lock,
+ ProcessID processType, double timestamp,
+ const nsACString& category,
+ const nsACString& method,
+ const nsACString& object,
+ const Maybe<nsCString>& value,
+ const ExtraArray& extra) {
+ // Look up the event id.
+ EventKey eventKey;
+ if (!GetEventKey(lock, category, method, object, &eventKey)) {
+ mozilla::Telemetry::AccumulateCategorical(
+ LABELS_TELEMETRY_EVENT_RECORDING_ERROR::UnknownEvent);
+ return RecordEventResult::UnknownEvent;
+ }
+
+ // If the event is expired or not enabled for this process, we silently drop
+ // this call. We don't want recording for expired probes to be an error so
+ // code doesn't have to be removed at a specific time or version. Even logging
+ // warnings would become very noisy.
+ if (IsExpired(eventKey)) {
+ mozilla::Telemetry::AccumulateCategorical(
+ LABELS_TELEMETRY_EVENT_RECORDING_ERROR::Expired);
+ return RecordEventResult::ExpiredEvent;
+ }
+
+ // Fixup the process id only for non-builtin (e.g. supporting build faster)
+ // dynamic events.
+ auto dynamicNonBuiltin =
+ eventKey.dynamic && !(*gDynamicEventInfo)[eventKey.id].builtin;
+ if (dynamicNonBuiltin) {
+ processType = ProcessID::Dynamic;
+ }
+
+ // Check whether the extra keys passed are valid.
+ if (!CheckExtraKeysValid(eventKey, extra)) {
+ mozilla::Telemetry::AccumulateCategorical(
+ LABELS_TELEMETRY_EVENT_RECORDING_ERROR::ExtraKey);
+ return RecordEventResult::InvalidExtraKey;
+ }
+
+ // Check whether we can record this event.
+ if (!CanRecordEvent(lock, eventKey, processType)) {
+ return RecordEventResult::CannotRecord;
+ }
+
+ // Count the number of times this event has been recorded, even if its
+ // category does not have recording enabled.
+ TelemetryScalar::SummarizeEvent(UniqueEventName(category, method, object),
+ processType, dynamicNonBuiltin);
+
+ // Check whether this event's category has recording enabled
+ if (!gEnabledCategories.Contains(GetCategory(lock, eventKey))) {
+ return RecordEventResult::Ok;
+ }
+
+ EventRecordArray* eventRecords = GetEventRecordsForProcess(lock, processType);
+ eventRecords->AppendElement(EventRecord(timestamp, eventKey, value, extra));
+
+ // Notify observers when we hit the "event" ping event record limit.
+ if (eventRecords->Length() == kEventPingLimit) {
+ return RecordEventResult::StorageLimitReached;
+ }
+
+ return RecordEventResult::Ok;
+}
+
+RecordEventResult ShouldRecordChildEvent(const StaticMutexAutoLock& lock,
+ const nsACString& category,
+ const nsACString& method,
+ const nsACString& object) {
+ EventKey eventKey;
+ if (!GetEventKey(lock, category, method, object, &eventKey)) {
+ // This event is unknown in this process, but it might be a dynamic event
+ // that was registered in the parent process.
+ return RecordEventResult::Ok;
+ }
+
+ if (IsExpired(eventKey)) {
+ return RecordEventResult::ExpiredEvent;
+ }
+
+ const auto processes =
+ gEventInfo[eventKey.id].common_info.record_in_processes;
+ if (!CanRecordInProcess(processes, XRE_GetProcessType())) {
+ return RecordEventResult::WrongProcess;
+ }
+
+ return RecordEventResult::Ok;
+}
+
+void RegisterEvents(const StaticMutexAutoLock& lock, const nsACString& category,
+ const nsTArray<DynamicEventInfo>& eventInfos,
+ const nsTArray<bool>& eventExpired, bool aBuiltin) {
+ MOZ_ASSERT(eventInfos.Length() == eventExpired.Length(),
+ "Event data array sizes should match.");
+
+ // Register the new events.
+ if (!gDynamicEventInfo) {
+ gDynamicEventInfo = new nsTArray<DynamicEventInfo>();
+ }
+
+ for (uint32_t i = 0, len = eventInfos.Length(); i < len; ++i) {
+ const nsCString& eventName = UniqueEventName(eventInfos[i]);
+
+ // Re-registering events can happen for two reasons and we don't print
+ // warnings:
+ //
+ // * When add-ons update.
+ // We don't support changing their definition, but the expiry might have
+ // changed.
+ // * When dynamic builtins ("build faster") events are registered.
+ // The dynamic definition takes precedence then.
+ EventKey existing;
+ if (!aBuiltin && gEventNameIDMap.Get(eventName, &existing)) {
+ if (eventExpired[i]) {
+ existing.id = kExpiredEventId;
+ }
+ continue;
+ }
+
+ gDynamicEventInfo->AppendElement(eventInfos[i]);
+ uint32_t eventId =
+ eventExpired[i] ? kExpiredEventId : gDynamicEventInfo->Length() - 1;
+ gEventNameIDMap.InsertOrUpdate(eventName, EventKey{eventId, true});
+ }
+
+ // If it is a builtin, add the category name in order to enable it later.
+ if (aBuiltin) {
+ gCategoryNames.Insert(category);
+ }
+
+ if (!aBuiltin) {
+ // Now after successful registration enable recording for this category
+ // (if not a dynamic builtin).
+ gEnabledCategories.Insert(category);
+ }
+}
+
+} // anonymous namespace
+
+////////////////////////////////////////////////////////////////////////
+////////////////////////////////////////////////////////////////////////
+//
+// PRIVATE: thread-unsafe helpers for event handling.
+
+namespace {
+
+nsresult SerializeEventsArray(const EventRecordArray& events, JSContext* cx,
+ JS::MutableHandle<JSObject*> result,
+ unsigned int dataset) {
+ // We serialize the events to a JS array.
+ JS::Rooted<JSObject*> eventsArray(cx,
+ JS::NewArrayObject(cx, events.Length()));
+ if (!eventsArray) {
+ return NS_ERROR_FAILURE;
+ }
+
+ for (uint32_t i = 0; i < events.Length(); ++i) {
+ const EventRecord& record = events[i];
+
+ // Each entry is an array of one of the forms:
+ // [timestamp, category, method, object, value]
+ // [timestamp, category, method, object, null, extra]
+ // [timestamp, category, method, object, value, extra]
+ JS::RootedVector<JS::Value> items(cx);
+
+ // Add timestamp.
+ JS::Rooted<JS::Value> val(cx);
+ if (!items.append(JS::NumberValue(floor(record.Timestamp())))) {
+ return NS_ERROR_FAILURE;
+ }
+
+ // Add category, method, object.
+ auto addCategoryMethodObjectValues = [&](const nsACString& category,
+ const nsACString& method,
+ const nsACString& object) -> bool {
+ return items.append(JS::StringValue(ToJSString(cx, category))) &&
+ items.append(JS::StringValue(ToJSString(cx, method))) &&
+ items.append(JS::StringValue(ToJSString(cx, object)));
+ };
+
+ const EventKey& eventKey = record.GetEventKey();
+ if (!eventKey.dynamic) {
+ const EventInfo& info = gEventInfo[eventKey.id];
+ if (!addCategoryMethodObjectValues(info.common_info.category(),
+ info.method(), info.object())) {
+ return NS_ERROR_FAILURE;
+ }
+ } else if (gDynamicEventInfo) {
+ const DynamicEventInfo& info = (*gDynamicEventInfo)[eventKey.id];
+ if (!addCategoryMethodObjectValues(info.category, info.method,
+ info.object)) {
+ return NS_ERROR_FAILURE;
+ }
+ }
+
+ // Add the optional string value only when needed.
+ // When the value field is empty and extra is not set, we can save a little
+ // space that way. We still need to submit a null value if extra is set, to
+ // match the form: [ts, category, method, object, null, extra]
+ if (record.Value()) {
+ if (!items.append(
+ JS::StringValue(ToJSString(cx, record.Value().value())))) {
+ return NS_ERROR_FAILURE;
+ }
+ } else if (!record.Extra().IsEmpty()) {
+ if (!items.append(JS::NullValue())) {
+ return NS_ERROR_FAILURE;
+ }
+ }
+
+ // Add the optional extra dictionary.
+ // To save a little space, only add it when it is not empty.
+ if (!record.Extra().IsEmpty()) {
+ JS::Rooted<JSObject*> obj(cx, JS_NewPlainObject(cx));
+ if (!obj) {
+ return NS_ERROR_FAILURE;
+ }
+
+ // Add extra key & value entries.
+ const ExtraArray& extra = record.Extra();
+ for (uint32_t i = 0; i < extra.Length(); ++i) {
+ JS::Rooted<JS::Value> value(cx);
+ value.setString(ToJSString(cx, extra[i].value));
+
+ if (!JS_DefineProperty(cx, obj, extra[i].key.get(), value,
+ JSPROP_ENUMERATE)) {
+ return NS_ERROR_FAILURE;
+ }
+ }
+ val.setObject(*obj);
+
+ if (!items.append(val)) {
+ return NS_ERROR_FAILURE;
+ }
+ }
+
+ // Add the record to the events array.
+ JS::Rooted<JSObject*> itemsArray(cx, JS::NewArrayObject(cx, items));
+ if (!JS_DefineElement(cx, eventsArray, i, itemsArray, JSPROP_ENUMERATE)) {
+ return NS_ERROR_FAILURE;
+ }
+ }
+
+ result.set(eventsArray);
+ return NS_OK;
+}
+
+} // anonymous namespace
+
+////////////////////////////////////////////////////////////////////////
+////////////////////////////////////////////////////////////////////////
+//
+// EXTERNALLY VISIBLE FUNCTIONS in namespace TelemetryEvents::
+
+// 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 gTelemetryEventsMutex MOZ_UNANNOTATED;
+
+void TelemetryEvent::InitializeGlobalState(bool aCanRecordBase,
+ bool aCanRecordExtended) {
+ StaticMutexAutoLock locker(gTelemetryEventsMutex);
+ MOZ_ASSERT(!gInitDone,
+ "TelemetryEvent::InitializeGlobalState "
+ "may only be called once");
+
+ gCanRecordBase = aCanRecordBase;
+ gCanRecordExtended = aCanRecordExtended;
+
+ // Populate the static event name->id cache. Note that the event names are
+ // statically allocated and come from the automatically generated
+ // TelemetryEventData.h.
+ const uint32_t eventCount =
+ static_cast<uint32_t>(mozilla::Telemetry::EventID::EventCount);
+ for (uint32_t i = 0; i < eventCount; ++i) {
+ const EventInfo& info = gEventInfo[i];
+ uint32_t eventId = i;
+
+ // If this event is expired or not recorded in this process, mark it with
+ // a special event id.
+ // This avoids doing repeated checks at runtime.
+ if (IsExpiredVersion(info.common_info.expiration_version().get())) {
+ eventId = kExpiredEventId;
+ }
+
+ gEventNameIDMap.InsertOrUpdate(UniqueEventName(info),
+ EventKey{eventId, false});
+ gCategoryNames.Insert(info.common_info.category());
+ }
+
+ // A hack until bug 1691156 is fixed
+ gEnabledCategories.Insert("avif"_ns);
+
+ gInitDone = true;
+}
+
+void TelemetryEvent::DeInitializeGlobalState() {
+ StaticMutexAutoLock locker(gTelemetryEventsMutex);
+ MOZ_ASSERT(gInitDone);
+
+ gCanRecordBase = false;
+ gCanRecordExtended = false;
+
+ gEventNameIDMap.Clear();
+ gCategoryNames.Clear();
+ gEnabledCategories.Clear();
+ gEventRecords.Clear();
+
+ gDynamicEventInfo = nullptr;
+
+ gInitDone = false;
+}
+
+void TelemetryEvent::SetCanRecordBase(bool b) {
+ StaticMutexAutoLock locker(gTelemetryEventsMutex);
+ gCanRecordBase = b;
+}
+
+void TelemetryEvent::SetCanRecordExtended(bool b) {
+ StaticMutexAutoLock locker(gTelemetryEventsMutex);
+ gCanRecordExtended = b;
+}
+
+nsresult TelemetryEvent::RecordChildEvents(
+ ProcessID aProcessType,
+ const nsTArray<mozilla::Telemetry::ChildEventData>& aEvents) {
+ MOZ_ASSERT(XRE_IsParentProcess());
+ StaticMutexAutoLock locker(gTelemetryEventsMutex);
+ for (uint32_t i = 0; i < aEvents.Length(); ++i) {
+ const mozilla::Telemetry::ChildEventData& e = aEvents[i];
+
+ // Timestamps from child processes are absolute. We fix them up here to be
+ // relative to the main process start time.
+ // This allows us to put events from all processes on the same timeline.
+ double relativeTimestamp =
+ (e.timestamp - TimeStamp::ProcessCreation()).ToMilliseconds();
+
+ ::RecordEvent(locker, aProcessType, relativeTimestamp, e.category, e.method,
+ e.object, e.value, e.extra);
+ }
+ return NS_OK;
+}
+
+nsresult TelemetryEvent::RecordEvent(const nsACString& aCategory,
+ const nsACString& aMethod,
+ const nsACString& aObject,
+ JS::Handle<JS::Value> aValue,
+ JS::Handle<JS::Value> aExtra,
+ JSContext* cx, uint8_t optional_argc) {
+ // Check value argument.
+ if ((optional_argc > 0) && !aValue.isNull() && !aValue.isString()) {
+ LogToBrowserConsole(nsIScriptError::warningFlag,
+ u"Invalid type for value parameter."_ns);
+ mozilla::Telemetry::AccumulateCategorical(
+ LABELS_TELEMETRY_EVENT_RECORDING_ERROR::Value);
+ return NS_OK;
+ }
+
+ // Extract value parameter.
+ Maybe<nsCString> value;
+ if (aValue.isString()) {
+ nsAutoJSString jsStr;
+ if (!jsStr.init(cx, aValue)) {
+ LogToBrowserConsole(nsIScriptError::warningFlag,
+ u"Invalid string value for value parameter."_ns);
+ mozilla::Telemetry::AccumulateCategorical(
+ LABELS_TELEMETRY_EVENT_RECORDING_ERROR::Value);
+ return NS_OK;
+ }
+
+ nsCString str = NS_ConvertUTF16toUTF8(jsStr);
+ if (str.Length() > kMaxValueByteLength) {
+ LogToBrowserConsole(
+ nsIScriptError::warningFlag,
+ nsLiteralString(
+ u"Value parameter exceeds maximum string length, truncating."));
+ TruncateToByteLength(str, kMaxValueByteLength);
+ }
+ value = mozilla::Some(str);
+ }
+
+ // Check extra argument.
+ if ((optional_argc > 1) && !aExtra.isNull() && !aExtra.isObject()) {
+ LogToBrowserConsole(nsIScriptError::warningFlag,
+ u"Invalid type for extra parameter."_ns);
+ mozilla::Telemetry::AccumulateCategorical(
+ LABELS_TELEMETRY_EVENT_RECORDING_ERROR::Extra);
+ return NS_OK;
+ }
+
+ // Extract extra dictionary.
+ ExtraArray extra;
+ if (aExtra.isObject()) {
+ JS::Rooted<JSObject*> obj(cx, &aExtra.toObject());
+ JS::Rooted<JS::IdVector> ids(cx, JS::IdVector(cx));
+ if (!JS_Enumerate(cx, obj, &ids)) {
+ LogToBrowserConsole(nsIScriptError::warningFlag,
+ u"Failed to enumerate object."_ns);
+ mozilla::Telemetry::AccumulateCategorical(
+ LABELS_TELEMETRY_EVENT_RECORDING_ERROR::Extra);
+ return NS_OK;
+ }
+
+ for (size_t i = 0, n = ids.length(); i < n; i++) {
+ nsAutoJSString key;
+ if (!key.init(cx, ids[i])) {
+ LogToBrowserConsole(
+ nsIScriptError::warningFlag,
+ nsLiteralString(
+ u"Extra dictionary should only contain string keys."));
+ mozilla::Telemetry::AccumulateCategorical(
+ LABELS_TELEMETRY_EVENT_RECORDING_ERROR::Extra);
+ return NS_OK;
+ }
+
+ JS::Rooted<JS::Value> value(cx);
+ if (!JS_GetPropertyById(cx, obj, ids[i], &value)) {
+ LogToBrowserConsole(nsIScriptError::warningFlag,
+ u"Failed to get extra property."_ns);
+ mozilla::Telemetry::AccumulateCategorical(
+ LABELS_TELEMETRY_EVENT_RECORDING_ERROR::Extra);
+ return NS_OK;
+ }
+
+ nsAutoJSString jsStr;
+ if (!value.isString() || !jsStr.init(cx, value)) {
+ LogToBrowserConsole(nsIScriptError::warningFlag,
+ u"Extra properties should have string values."_ns);
+ mozilla::Telemetry::AccumulateCategorical(
+ LABELS_TELEMETRY_EVENT_RECORDING_ERROR::Extra);
+ return NS_OK;
+ }
+
+ nsCString str = NS_ConvertUTF16toUTF8(jsStr);
+ if (str.Length() > kMaxExtraValueByteLength) {
+ LogToBrowserConsole(
+ nsIScriptError::warningFlag,
+ nsLiteralString(
+ u"Extra value exceeds maximum string length, truncating."));
+ TruncateToByteLength(str, kMaxExtraValueByteLength);
+ }
+
+ extra.AppendElement(EventExtraEntry{NS_ConvertUTF16toUTF8(key), str});
+ }
+ }
+
+ // Lock for accessing internal data.
+ // While the lock is being held, no complex calls like JS calls can be made,
+ // as all of these could record Telemetry, which would result in deadlock.
+ RecordEventResult res;
+ if (!XRE_IsParentProcess()) {
+ {
+ StaticMutexAutoLock lock(gTelemetryEventsMutex);
+ res = ::ShouldRecordChildEvent(lock, aCategory, aMethod, aObject);
+ }
+
+ if (res == RecordEventResult::Ok) {
+ TelemetryIPCAccumulator::RecordChildEvent(
+ TimeStamp::NowLoRes(), aCategory, aMethod, aObject, value, extra);
+ }
+ } else {
+ StaticMutexAutoLock lock(gTelemetryEventsMutex);
+
+ if (!gInitDone) {
+ return NS_ERROR_FAILURE;
+ }
+
+ // Get the current time.
+ double timestamp = -1;
+ if (NS_WARN_IF(NS_FAILED(MsSinceProcessStart(&timestamp)))) {
+ return NS_ERROR_FAILURE;
+ }
+
+ res = ::RecordEvent(lock, ProcessID::Parent, timestamp, aCategory, aMethod,
+ aObject, value, extra);
+ }
+
+ // Trigger warnings or errors where needed.
+ switch (res) {
+ case RecordEventResult::UnknownEvent: {
+ nsPrintfCString msg(R"(Unknown event: ["%s", "%s", "%s"])",
+ PromiseFlatCString(aCategory).get(),
+ PromiseFlatCString(aMethod).get(),
+ PromiseFlatCString(aObject).get());
+ LogToBrowserConsole(nsIScriptError::errorFlag,
+ NS_ConvertUTF8toUTF16(msg));
+ return NS_OK;
+ }
+ case RecordEventResult::InvalidExtraKey: {
+ nsPrintfCString msg(R"(Invalid extra key for event ["%s", "%s", "%s"].)",
+ PromiseFlatCString(aCategory).get(),
+ PromiseFlatCString(aMethod).get(),
+ PromiseFlatCString(aObject).get());
+ LogToBrowserConsole(nsIScriptError::warningFlag,
+ NS_ConvertUTF8toUTF16(msg));
+ return NS_OK;
+ }
+ case RecordEventResult::StorageLimitReached: {
+ LogToBrowserConsole(nsIScriptError::warningFlag,
+ u"Event storage limit reached."_ns);
+ nsCOMPtr<nsIObserverService> serv =
+ mozilla::services::GetObserverService();
+ if (serv) {
+ serv->NotifyObservers(nullptr, "event-telemetry-storage-limit-reached",
+ nullptr);
+ }
+ return NS_OK;
+ }
+ default:
+ return NS_OK;
+ }
+}
+
+void TelemetryEvent::RecordEventNative(
+ mozilla::Telemetry::EventID aId, const mozilla::Maybe<nsCString>& aValue,
+ const mozilla::Maybe<ExtraArray>& aExtra) {
+ // Truncate aValue if present and necessary.
+ mozilla::Maybe<nsCString> value;
+ if (aValue) {
+ nsCString valueStr = aValue.ref();
+ if (valueStr.Length() > kMaxValueByteLength) {
+ TruncateToByteLength(valueStr, kMaxValueByteLength);
+ }
+ value = mozilla::Some(valueStr);
+ }
+
+ // Truncate any over-long extra values.
+ ExtraArray extra;
+ if (aExtra) {
+ extra = aExtra.value();
+ for (auto& item : extra) {
+ if (item.value.Length() > kMaxExtraValueByteLength) {
+ TruncateToByteLength(item.value, kMaxExtraValueByteLength);
+ }
+ }
+ }
+
+ const EventInfo& info = gEventInfo[static_cast<uint32_t>(aId)];
+ const nsCString category(info.common_info.category());
+ const nsCString method(info.method());
+ const nsCString object(info.object());
+ if (!XRE_IsParentProcess()) {
+ RecordEventResult res;
+ {
+ StaticMutexAutoLock lock(gTelemetryEventsMutex);
+ res = ::ShouldRecordChildEvent(lock, category, method, object);
+ }
+
+ if (res == RecordEventResult::Ok) {
+ TelemetryIPCAccumulator::RecordChildEvent(TimeStamp::NowLoRes(), category,
+ method, object, value, extra);
+ }
+ } else {
+ StaticMutexAutoLock lock(gTelemetryEventsMutex);
+
+ if (!gInitDone) {
+ return;
+ }
+
+ // Get the current time.
+ double timestamp = -1;
+ if (NS_WARN_IF(NS_FAILED(MsSinceProcessStart(&timestamp)))) {
+ return;
+ }
+
+ ::RecordEvent(lock, ProcessID::Parent, timestamp, category, method, object,
+ value, extra);
+ }
+}
+
+static bool GetArrayPropertyValues(JSContext* cx, JS::Handle<JSObject*> obj,
+ const char* property,
+ nsTArray<nsCString>* results) {
+ JS::Rooted<JS::Value> value(cx);
+ if (!JS_GetProperty(cx, obj, property, &value)) {
+ JS_ReportErrorASCII(cx, R"(Missing required property "%s" for event)",
+ property);
+ return false;
+ }
+
+ bool isArray = false;
+ if (!JS::IsArrayObject(cx, value, &isArray) || !isArray) {
+ JS_ReportErrorASCII(cx, R"(Property "%s" for event should be an array)",
+ property);
+ return false;
+ }
+
+ JS::Rooted<JSObject*> arrayObj(cx, &value.toObject());
+ uint32_t arrayLength;
+ if (!JS::GetArrayLength(cx, arrayObj, &arrayLength)) {
+ return false;
+ }
+
+ for (uint32_t arrayIdx = 0; arrayIdx < arrayLength; ++arrayIdx) {
+ JS::Rooted<JS::Value> element(cx);
+ if (!JS_GetElement(cx, arrayObj, arrayIdx, &element)) {
+ return false;
+ }
+
+ if (!element.isString()) {
+ JS_ReportErrorASCII(
+ cx, R"(Array entries for event property "%s" should be strings)",
+ property);
+ return false;
+ }
+
+ nsAutoJSString jsStr;
+ if (!jsStr.init(cx, element)) {
+ return false;
+ }
+
+ results->AppendElement(NS_ConvertUTF16toUTF8(jsStr));
+ }
+
+ return true;
+}
+
+nsresult TelemetryEvent::RegisterEvents(const nsACString& aCategory,
+ JS::Handle<JS::Value> aEventData,
+ bool aBuiltin, JSContext* cx) {
+ MOZ_ASSERT(XRE_IsParentProcess(),
+ "Events can only be registered in the parent process");
+
+ if (!IsValidIdentifierString(aCategory, 30, true, true)) {
+ JS_ReportErrorASCII(
+ cx, "Category parameter should match the identifier pattern.");
+ mozilla::Telemetry::AccumulateCategorical(
+ LABELS_TELEMETRY_EVENT_REGISTRATION_ERROR::Category);
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ if (!aEventData.isObject()) {
+ JS_ReportErrorASCII(cx, "Event data parameter should be an object");
+ mozilla::Telemetry::AccumulateCategorical(
+ LABELS_TELEMETRY_EVENT_REGISTRATION_ERROR::Other);
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ JS::Rooted<JSObject*> obj(cx, &aEventData.toObject());
+ JS::Rooted<JS::IdVector> eventPropertyIds(cx, JS::IdVector(cx));
+ if (!JS_Enumerate(cx, obj, &eventPropertyIds)) {
+ mozilla::Telemetry::AccumulateCategorical(
+ LABELS_TELEMETRY_EVENT_REGISTRATION_ERROR::Other);
+ return NS_ERROR_FAILURE;
+ }
+
+ // Collect the event data into local storage first.
+ // Only after successfully validating all contained events will we register
+ // them into global storage.
+ nsTArray<DynamicEventInfo> newEventInfos;
+ nsTArray<bool> newEventExpired;
+
+ for (size_t i = 0, n = eventPropertyIds.length(); i < n; i++) {
+ nsAutoJSString eventName;
+ if (!eventName.init(cx, eventPropertyIds[i])) {
+ mozilla::Telemetry::AccumulateCategorical(
+ LABELS_TELEMETRY_EVENT_REGISTRATION_ERROR::Other);
+ return NS_ERROR_FAILURE;
+ }
+
+ if (!IsValidIdentifierString(NS_ConvertUTF16toUTF8(eventName),
+ kMaxMethodNameByteLength, false, true)) {
+ JS_ReportErrorASCII(cx,
+ "Event names should match the identifier pattern.");
+ mozilla::Telemetry::AccumulateCategorical(
+ LABELS_TELEMETRY_EVENT_REGISTRATION_ERROR::Name);
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ JS::Rooted<JS::Value> value(cx);
+ if (!JS_GetPropertyById(cx, obj, eventPropertyIds[i], &value) ||
+ !value.isObject()) {
+ mozilla::Telemetry::AccumulateCategorical(
+ LABELS_TELEMETRY_EVENT_REGISTRATION_ERROR::Other);
+ return NS_ERROR_FAILURE;
+ }
+ JS::Rooted<JSObject*> eventObj(cx, &value.toObject());
+
+ // Extract the event registration data.
+ nsTArray<nsCString> methods;
+ nsTArray<nsCString> objects;
+ nsTArray<nsCString> extra_keys;
+ bool expired = false;
+ bool recordOnRelease = false;
+
+ // The methods & objects properties are required.
+ if (!GetArrayPropertyValues(cx, eventObj, "methods", &methods)) {
+ mozilla::Telemetry::AccumulateCategorical(
+ LABELS_TELEMETRY_EVENT_REGISTRATION_ERROR::Other);
+ return NS_ERROR_FAILURE;
+ }
+
+ if (!GetArrayPropertyValues(cx, eventObj, "objects", &objects)) {
+ mozilla::Telemetry::AccumulateCategorical(
+ LABELS_TELEMETRY_EVENT_REGISTRATION_ERROR::Other);
+ return NS_ERROR_FAILURE;
+ }
+
+ // extra_keys is optional.
+ bool hasProperty = false;
+ if (JS_HasProperty(cx, eventObj, "extra_keys", &hasProperty) &&
+ hasProperty) {
+ if (!GetArrayPropertyValues(cx, eventObj, "extra_keys", &extra_keys)) {
+ mozilla::Telemetry::AccumulateCategorical(
+ LABELS_TELEMETRY_EVENT_REGISTRATION_ERROR::Other);
+ return NS_ERROR_FAILURE;
+ }
+ }
+
+ // expired is optional.
+ if (JS_HasProperty(cx, eventObj, "expired", &hasProperty) && hasProperty) {
+ JS::Rooted<JS::Value> temp(cx);
+ if (!JS_GetProperty(cx, eventObj, "expired", &temp) ||
+ !temp.isBoolean()) {
+ mozilla::Telemetry::AccumulateCategorical(
+ LABELS_TELEMETRY_EVENT_REGISTRATION_ERROR::Other);
+ return NS_ERROR_FAILURE;
+ }
+
+ expired = temp.toBoolean();
+ }
+
+ // record_on_release is optional.
+ if (JS_HasProperty(cx, eventObj, "record_on_release", &hasProperty) &&
+ hasProperty) {
+ JS::Rooted<JS::Value> temp(cx);
+ if (!JS_GetProperty(cx, eventObj, "record_on_release", &temp) ||
+ !temp.isBoolean()) {
+ mozilla::Telemetry::AccumulateCategorical(
+ LABELS_TELEMETRY_EVENT_REGISTRATION_ERROR::Other);
+ return NS_ERROR_FAILURE;
+ }
+
+ recordOnRelease = temp.toBoolean();
+ }
+
+ // Validate methods.
+ for (auto& method : methods) {
+ if (!IsValidIdentifierString(method, kMaxMethodNameByteLength, false,
+ true)) {
+ JS_ReportErrorASCII(
+ cx, "Method names should match the identifier pattern.");
+ mozilla::Telemetry::AccumulateCategorical(
+ LABELS_TELEMETRY_EVENT_REGISTRATION_ERROR::Method);
+ return NS_ERROR_INVALID_ARG;
+ }
+ }
+
+ // Validate objects.
+ for (auto& object : objects) {
+ if (!IsValidIdentifierString(object, kMaxObjectNameByteLength, false,
+ true)) {
+ JS_ReportErrorASCII(
+ cx, "Object names should match the identifier pattern.");
+ mozilla::Telemetry::AccumulateCategorical(
+ LABELS_TELEMETRY_EVENT_REGISTRATION_ERROR::Object);
+ return NS_ERROR_INVALID_ARG;
+ }
+ }
+
+ // Validate extra keys.
+ if (extra_keys.Length() > kMaxExtraKeyCount) {
+ JS_ReportErrorASCII(cx, "No more than 10 extra keys can be registered.");
+ mozilla::Telemetry::AccumulateCategorical(
+ LABELS_TELEMETRY_EVENT_REGISTRATION_ERROR::ExtraKeys);
+ return NS_ERROR_INVALID_ARG;
+ }
+ for (auto& key : extra_keys) {
+ if (!IsValidIdentifierString(key, kMaxExtraKeyNameByteLength, false,
+ true)) {
+ JS_ReportErrorASCII(
+ cx, "Extra key names should match the identifier pattern.");
+ mozilla::Telemetry::AccumulateCategorical(
+ LABELS_TELEMETRY_EVENT_REGISTRATION_ERROR::ExtraKeys);
+ return NS_ERROR_INVALID_ARG;
+ }
+ }
+
+ // Append event infos to be registered.
+ for (auto& method : methods) {
+ for (auto& object : objects) {
+ // 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.
+ DynamicEventInfo info{aCategory, method, object,
+ extra_keys, recordOnRelease, aBuiltin};
+ newEventInfos.AppendElement(info);
+ newEventExpired.AppendElement(expired);
+ }
+ }
+ }
+
+ {
+ StaticMutexAutoLock locker(gTelemetryEventsMutex);
+ RegisterEvents(locker, aCategory, newEventInfos, newEventExpired, aBuiltin);
+ }
+
+ return NS_OK;
+}
+
+nsresult TelemetryEvent::CreateSnapshots(uint32_t aDataset, bool aClear,
+ uint32_t aEventLimit, JSContext* cx,
+ uint8_t optional_argc,
+ JS::MutableHandle<JS::Value> aResult) {
+ if (!XRE_IsParentProcess()) {
+ return NS_ERROR_FAILURE;
+ }
+
+ // Creating a JS snapshot of the events is a two-step process:
+ // (1) Lock the storage and copy the events into function-local storage.
+ // (2) Serialize the events into JS.
+ // We can't hold a lock for (2) because we will run into deadlocks otherwise
+ // from JS recording Telemetry.
+
+ // (1) Extract the events from storage with a lock held.
+ nsTArray<std::pair<const char*, EventRecordArray>> processEvents;
+ nsTArray<std::pair<uint32_t, EventRecordArray>> leftovers;
+ {
+ StaticMutexAutoLock locker(gTelemetryEventsMutex);
+
+ if (!gInitDone) {
+ return NS_ERROR_FAILURE;
+ }
+
+ // The snapshotting function is the same for both static and dynamic builtin
+ // events. We can use the same function and store the events in the same
+ // output storage.
+ auto snapshotter = [aDataset, &locker, &processEvents, &leftovers, aClear,
+ optional_argc,
+ aEventLimit](EventRecordsMapType& aProcessStorage) {
+ for (const auto& entry : aProcessStorage) {
+ const EventRecordArray* eventStorage = entry.GetWeak();
+ EventRecordArray events;
+ EventRecordArray leftoverEvents;
+
+ const uint32_t len = eventStorage->Length();
+ for (uint32_t i = 0; i < len; ++i) {
+ const EventRecord& record = (*eventStorage)[i];
+ if (IsInDataset(GetDataset(locker, record.GetEventKey()), aDataset)) {
+ // If we have a limit, adhere to it. If we have a limit and are
+ // going to clear, save the leftovers for later.
+ if (optional_argc < 2 || events.Length() < aEventLimit) {
+ events.AppendElement(record);
+ } else if (aClear) {
+ leftoverEvents.AppendElement(record);
+ }
+ }
+ }
+
+ if (events.Length()) {
+ const char* processName =
+ GetNameForProcessID(ProcessID(entry.GetKey()));
+ processEvents.EmplaceBack(processName, std::move(events));
+ if (leftoverEvents.Length()) {
+ leftovers.EmplaceBack(entry.GetKey(), std::move(leftoverEvents));
+ }
+ }
+ }
+ };
+
+ // Take a snapshot of the plain and dynamic builtin events.
+ snapshotter(gEventRecords);
+ if (aClear) {
+ gEventRecords.Clear();
+ for (auto& pair : leftovers) {
+ gEventRecords.InsertOrUpdate(
+ pair.first, MakeUnique<EventRecordArray>(std::move(pair.second)));
+ }
+ leftovers.Clear();
+ }
+ }
+
+ // (2) Serialize the events to a JS object.
+ JS::Rooted<JSObject*> rootObj(cx, JS_NewPlainObject(cx));
+ if (!rootObj) {
+ return NS_ERROR_FAILURE;
+ }
+
+ const uint32_t processLength = processEvents.Length();
+ for (uint32_t i = 0; i < processLength; ++i) {
+ JS::Rooted<JSObject*> eventsArray(cx);
+ if (NS_FAILED(SerializeEventsArray(processEvents[i].second, cx,
+ &eventsArray, aDataset))) {
+ return NS_ERROR_FAILURE;
+ }
+
+ if (!JS_DefineProperty(cx, rootObj, processEvents[i].first, eventsArray,
+ JSPROP_ENUMERATE)) {
+ return NS_ERROR_FAILURE;
+ }
+ }
+
+ aResult.setObject(*rootObj);
+ return NS_OK;
+}
+
+/**
+ * Resets all the stored events. This is intended to be only used in tests.
+ */
+void TelemetryEvent::ClearEvents() {
+ StaticMutexAutoLock lock(gTelemetryEventsMutex);
+
+ if (!gInitDone) {
+ return;
+ }
+
+ gEventRecords.Clear();
+}
+
+void TelemetryEvent::SetEventRecordingEnabled(const nsACString& category,
+ bool enabled) {
+ StaticMutexAutoLock locker(gTelemetryEventsMutex);
+
+ if (!gCategoryNames.Contains(category)) {
+ LogToBrowserConsole(
+ nsIScriptError::warningFlag,
+ NS_ConvertUTF8toUTF16(
+ nsLiteralCString(
+ "Unknown category for SetEventRecordingEnabled: ") +
+ category));
+ return;
+ }
+
+ if (enabled) {
+ gEnabledCategories.Insert(category);
+ } else {
+ gEnabledCategories.Remove(category);
+ }
+}
+
+size_t TelemetryEvent::SizeOfIncludingThis(
+ mozilla::MallocSizeOf aMallocSizeOf) {
+ StaticMutexAutoLock locker(gTelemetryEventsMutex);
+ size_t n = 0;
+
+ auto getSizeOfRecords = [aMallocSizeOf](auto& storageMap) {
+ size_t partial = storageMap.ShallowSizeOfExcludingThis(aMallocSizeOf);
+ for (const auto& eventRecords : storageMap.Values()) {
+ partial += eventRecords->ShallowSizeOfIncludingThis(aMallocSizeOf);
+
+ const uint32_t len = eventRecords->Length();
+ for (uint32_t i = 0; i < len; ++i) {
+ partial += (*eventRecords)[i].SizeOfExcludingThis(aMallocSizeOf);
+ }
+ }
+ return partial;
+ };
+
+ n += getSizeOfRecords(gEventRecords);
+
+ n += gEventNameIDMap.ShallowSizeOfExcludingThis(aMallocSizeOf);
+ for (auto iter = gEventNameIDMap.ConstIter(); !iter.Done(); iter.Next()) {
+ n += iter.Key().SizeOfExcludingThisIfUnshared(aMallocSizeOf);
+ }
+
+ n += gCategoryNames.ShallowSizeOfExcludingThis(aMallocSizeOf);
+ n += gEnabledCategories.ShallowSizeOfExcludingThis(aMallocSizeOf);
+
+ if (gDynamicEventInfo) {
+ n += gDynamicEventInfo->ShallowSizeOfIncludingThis(aMallocSizeOf);
+ for (auto& info : *gDynamicEventInfo) {
+ n += info.SizeOfExcludingThis(aMallocSizeOf);
+ }
+ }
+
+ return n;
+}