/* -*- 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 #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(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::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& 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 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 ExtraArray; class EventRecord { public: EventRecord(double timestamp, const EventKey& key, const Maybe& 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& 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 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 gEventNameIDMap(kEventCount); // The CategoryName set. nsTHashSet gCategoryNames; // This tracks the IDs of the categories for which recording is enabled. nsTHashSet gEnabledCategories; // The main event storage. Events are inserted here, keyed by process id and // in recording order. typedef nsUint32HashKey ProcessIDHashKey; typedef nsTArray EventRecordArray; typedef nsClassHashtable EventRecordsMapType; EventRecordsMapType gEventRecords; // The details on dynamic events that are recorded from addons are registered // here. StaticAutoPtr> 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 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& 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& eventInfos, const nsTArray& eventExpired, bool aBuiltin) { MOZ_ASSERT(eventInfos.Length() == eventExpired.Length(), "Event data array sizes should match."); // Register the new events. if (!gDynamicEventInfo) { gDynamicEventInfo = new nsTArray(); } 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 result, unsigned int dataset) { // We serialize the events to a JS array. JS::Rooted 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 items(cx); // Add timestamp. JS::Rooted 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 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 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 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(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& 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 aValue, JS::Handle 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 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 obj(cx, &aExtra.toObject()); JS::Rooted 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 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(×tamp)))) { 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 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& aValue, const mozilla::Maybe& aExtra) { // Truncate aValue if present and necessary. mozilla::Maybe 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(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(×tamp)))) { return; } ::RecordEvent(lock, ProcessID::Parent, timestamp, category, method, object, value, extra); } } static bool GetArrayPropertyValues(JSContext* cx, JS::Handle obj, const char* property, nsTArray* results) { JS::Rooted 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 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 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 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 obj(cx, &aEventData.toObject()); JS::Rooted 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 newEventInfos; nsTArray 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 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 eventObj(cx, &value.toObject()); // Extract the event registration data. nsTArray methods; nsTArray objects; nsTArray 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 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 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 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> processEvents; nsTArray> 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(std::move(pair.second))); } leftovers.Clear(); } } // (2) Serialize the events to a JS object. JS::Rooted 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 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; }