diff options
Diffstat (limited to 'toolkit/components/telemetry/TelemetryEvent.cpp')
-rw-r--r-- | toolkit/components/telemetry/TelemetryEvent.cpp | 687 |
1 files changed, 687 insertions, 0 deletions
diff --git a/toolkit/components/telemetry/TelemetryEvent.cpp b/toolkit/components/telemetry/TelemetryEvent.cpp new file mode 100644 index 000000000..1e8126f66 --- /dev/null +++ b/toolkit/components/telemetry/TelemetryEvent.cpp @@ -0,0 +1,687 @@ +/* -*- 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 <prtime.h> +#include "nsITelemetry.h" +#include "nsHashKeys.h" +#include "nsDataHashtable.h" +#include "nsClassHashtable.h" +#include "nsTArray.h" +#include "mozilla/StaticMutex.h" +#include "mozilla/Unused.h" +#include "mozilla/Maybe.h" +#include "mozilla/StaticPtr.h" +#include "jsapi.h" +#include "nsJSUtils.h" +#include "nsXULAppAPI.h" +#include "nsUTF8Utils.h" + +#include "TelemetryCommon.h" +#include "TelemetryEvent.h" +#include "TelemetryEventData.h" + +using mozilla::StaticMutex; +using mozilla::StaticMutexAutoLock; +using mozilla::ArrayLength; +using mozilla::Maybe; +using mozilla::Nothing; +using mozilla::Pair; +using mozilla::StaticAutoPtr; +using mozilla::Telemetry::Common::AutoHashtable; +using mozilla::Telemetry::Common::IsExpiredVersion; +using mozilla::Telemetry::Common::CanRecordDataset; +using mozilla::Telemetry::Common::IsInDataset; +using mozilla::Telemetry::Common::MsSinceProcessStart; +using mozilla::Telemetry::Common::LogToBrowserConsole; + +//////////////////////////////////////////////////////////////////////// +//////////////////////////////////////////////////////////////////////// +// +// 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 = mozilla::Telemetry::EventID::EventCount; +// This is a special event id used to mark expired events, to make expiry checks +// faster at runtime. +const uint32_t kExpiredEventId = kEventCount + 1; +static_assert(kEventCount < kExpiredEventId, "Should not overflow."); + +// This is the hard upper limit on the number of event records we keep in storage. +// If we cross this limit, we will drop any further event recording until elements +// are removed from storage. +const uint32_t kMaxEventRecords = 1000; +// 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; + +typedef nsDataHashtable<nsCStringHashKey, uint32_t> EventMapType; +typedef nsClassHashtable<nsCStringHashKey, nsCString> StringMap; + +enum class RecordEventResult { + Ok, + UnknownEvent, + InvalidExtraKey, + StorageLimitReached, +}; + +struct ExtraEntry { + const nsCString key; + const nsCString value; +}; + +typedef nsTArray<ExtraEntry> ExtraArray; + +class EventRecord { +public: + EventRecord(double timestamp, uint32_t eventId, const Maybe<nsCString>& value, + const ExtraArray& extra) + : mTimestamp(timestamp) + , mEventId(eventId) + , mValue(value) + , mExtra(extra) + {} + + EventRecord(const EventRecord& other) + : mTimestamp(other.mTimestamp) + , mEventId(other.mEventId) + , mValue(other.mValue) + , mExtra(other.mExtra) + {} + + EventRecord& operator=(const EventRecord& other) = delete; + + double Timestamp() const { return mTimestamp; } + uint32_t EventId() const { return mEventId; } + 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 uint32_t mEventId; + const Maybe<nsCString> mValue; + const ExtraArray mExtra; +}; + +// Implements the methods for EventInfo. +const char* +EventInfo::method() const +{ + return &gEventsStringTable[this->method_offset]; +} + +const char* +EventInfo::object() const +{ + return &gEventsStringTable[this->object_offset]; +} + +// Implements the methods for CommonEventInfo. +const char* +CommonEventInfo::category() const +{ + return &gEventsStringTable[this->category_offset]; +} + +const char* +CommonEventInfo::expiration_version() const +{ + return &gEventsStringTable[this->expiration_version_offset]; +} + +const char* +CommonEventInfo::extra_key(uint32_t index) const +{ + MOZ_ASSERT(index < this->extra_count); + uint32_t key_index = gExtraKeysTable[this->extra_index + index]; + return &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(nsDependentCString(info.common_info.category()), + nsDependentCString(info.method()), + nsDependentCString(info.object())); +} + +bool +IsExpiredDate(uint32_t expires_days_since_epoch) { + if (expires_days_since_epoch == 0) { + return false; + } + + const uint32_t days_since_epoch = PR_Now() / (PRTime(PR_USEC_PER_SEC) * 24 * 60 * 60); + return expires_days_since_epoch <= days_since_epoch; +} + +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 Name -> ID cache map. +EventMapType gEventNameIDMap(kEventCount); + +// The main event storage. Events are inserted here in recording order. +StaticAutoPtr<nsTArray<EventRecord>> gEventRecords; + +} // namespace + +//////////////////////////////////////////////////////////////////////// +//////////////////////////////////////////////////////////////////////// +// +// PRIVATE: thread-unsafe helpers for event recording. + +namespace { + +bool +CanRecordEvent(const StaticMutexAutoLock& lock, const CommonEventInfo& info) +{ + if (!gCanRecordBase) { + return false; + } + + return CanRecordDataset(info.dataset, gCanRecordBase, gCanRecordExtended); +} + +RecordEventResult +RecordEvent(const StaticMutexAutoLock& lock, double timestamp, + const nsACString& category, const nsACString& method, + const nsACString& object, const Maybe<nsCString>& value, + const ExtraArray& extra) +{ + // Apply hard limit on event count in storage. + if (gEventRecords->Length() >= kMaxEventRecords) { + return RecordEventResult::StorageLimitReached; + } + + // Look up the event id. + const nsCString& name = UniqueEventName(category, method, object); + uint32_t eventId; + if (!gEventNameIDMap.Get(name, &eventId)) { + return RecordEventResult::UnknownEvent; + } + + // If the event is expired, 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 (eventId == kExpiredEventId) { + return RecordEventResult::Ok; + } + + // Check whether we can record this event. + const CommonEventInfo& common = gEventInfo[eventId].common_info; + if (!CanRecordEvent(lock, common)) { + return RecordEventResult::Ok; + } + + // Check whether the extra keys passed are valid. + nsTHashtable<nsCStringHashKey> validExtraKeys; + for (uint32_t i = 0; i < common.extra_count; ++i) { + validExtraKeys.PutEntry(nsDependentCString(common.extra_key(i))); + } + + for (uint32_t i = 0; i < extra.Length(); ++i) { + if (!validExtraKeys.GetEntry(extra[i].key)) { + return RecordEventResult::InvalidExtraKey; + } + } + + // Add event record. + gEventRecords->AppendElement(EventRecord(timestamp, eventId, value, extra)); + return RecordEventResult::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; + +void +TelemetryEvent::InitializeGlobalState(bool aCanRecordBase, bool aCanRecordExtended) +{ + StaticMutexAutoLock locker(gTelemetryEventsMutex); + MOZ_ASSERT(!gInitDone, "TelemetryEvent::InitializeGlobalState " + "may only be called once"); + + gCanRecordBase = aCanRecordBase; + gCanRecordExtended = aCanRecordExtended; + + gEventRecords = new nsTArray<EventRecord>(); + + // 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, mark it with a special event id. + // This avoids doing expensive expiry checks at runtime. + if (IsExpiredVersion(info.common_info.expiration_version()) || + IsExpiredDate(info.common_info.expiration_day)) { + eventId = kExpiredEventId; + } + + gEventNameIDMap.Put(UniqueEventName(info), eventId); + } + +#ifdef DEBUG + gEventNameIDMap.MarkImmutable(); +#endif + gInitDone = true; +} + +void +TelemetryEvent::DeInitializeGlobalState() +{ + StaticMutexAutoLock locker(gTelemetryEventsMutex); + MOZ_ASSERT(gInitDone); + + gCanRecordBase = false; + gCanRecordExtended = false; + + gEventNameIDMap.Clear(); + gEventRecords->Clear(); + gEventRecords = 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::RecordEvent(const nsACString& aCategory, const nsACString& aMethod, + const nsACString& aObject, JS::HandleValue aValue, + JS::HandleValue aExtra, JSContext* cx, + uint8_t optional_argc) +{ + // Currently only recording in the parent process is supported. + if (!XRE_IsParentProcess()) { + return NS_OK; + } + + // Get the current time. + double timestamp = -1; + nsresult rv = MsSinceProcessStart(×tamp); + if (NS_FAILED(rv)) { + LogToBrowserConsole(nsIScriptError::warningFlag, + NS_LITERAL_STRING("Failed to get time since process start.")); + return NS_OK; + } + + // Check value argument. + if ((optional_argc > 0) && !aValue.isNull() && !aValue.isString()) { + LogToBrowserConsole(nsIScriptError::warningFlag, + NS_LITERAL_STRING("Invalid type for value parameter.")); + return NS_OK; + } + + // Extract value parameter. + Maybe<nsCString> value; + if (aValue.isString()) { + nsAutoJSString jsStr; + if (!jsStr.init(cx, aValue)) { + LogToBrowserConsole(nsIScriptError::warningFlag, + NS_LITERAL_STRING("Invalid string value for value parameter.")); + return NS_OK; + } + + nsCString str = NS_ConvertUTF16toUTF8(jsStr); + if (str.Length() > kMaxValueByteLength) { + LogToBrowserConsole(nsIScriptError::warningFlag, + NS_LITERAL_STRING("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, + NS_LITERAL_STRING("Invalid type for extra parameter.")); + return NS_OK; + } + + // Extract extra dictionary. + ExtraArray extra; + if (aExtra.isObject()) { + JS::RootedObject obj(cx, &aExtra.toObject()); + JS::Rooted<JS::IdVector> ids(cx, JS::IdVector(cx)); + if (!JS_Enumerate(cx, obj, &ids)) { + LogToBrowserConsole(nsIScriptError::warningFlag, + NS_LITERAL_STRING("Failed to enumerate object.")); + 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, + NS_LITERAL_STRING("Extra dictionary should only contain string keys.")); + return NS_OK; + } + + JS::Rooted<JS::Value> value(cx); + if (!JS_GetPropertyById(cx, obj, ids[i], &value)) { + LogToBrowserConsole(nsIScriptError::warningFlag, + NS_LITERAL_STRING("Failed to get extra property.")); + return NS_OK; + } + + nsAutoJSString jsStr; + if (!value.isString() || !jsStr.init(cx, value)) { + LogToBrowserConsole(nsIScriptError::warningFlag, + NS_LITERAL_STRING("Extra properties should have string values.")); + return NS_OK; + } + + nsCString str = NS_ConvertUTF16toUTF8(jsStr); + if (str.Length() > kMaxExtraValueByteLength) { + LogToBrowserConsole(nsIScriptError::warningFlag, + NS_LITERAL_STRING("Extra value exceeds maximum string length, truncating.")); + TruncateToByteLength(str, kMaxExtraValueByteLength); + } + + extra.AppendElement(ExtraEntry{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; + { + StaticMutexAutoLock lock(gTelemetryEventsMutex); + + if (!gInitDone) { + return NS_ERROR_FAILURE; + } + + res = ::RecordEvent(lock, timestamp, aCategory, aMethod, aObject, value, extra); + } + + // Trigger warnings or errors where needed. + switch (res) { + case RecordEventResult::UnknownEvent: { + JS_ReportErrorASCII(cx, R"(Unknown event: ["%s", "%s", "%s"])", + PromiseFlatCString(aCategory).get(), + PromiseFlatCString(aMethod).get(), + PromiseFlatCString(aObject).get()); + return NS_ERROR_INVALID_ARG; + } + case RecordEventResult::InvalidExtraKey: + LogToBrowserConsole(nsIScriptError::warningFlag, + NS_LITERAL_STRING("Invalid extra key for event.")); + return NS_OK; + case RecordEventResult::StorageLimitReached: + LogToBrowserConsole(nsIScriptError::warningFlag, + NS_LITERAL_STRING("Event storage limit reached.")); + return NS_OK; + default: + return NS_OK; + } +} + +nsresult +TelemetryEvent::CreateSnapshots(uint32_t aDataset, bool aClear, JSContext* cx, + uint8_t optional_argc, JS::MutableHandleValue aResult) +{ + // Extract the events from storage. + nsTArray<EventRecord> events; + { + StaticMutexAutoLock locker(gTelemetryEventsMutex); + + if (!gInitDone) { + return NS_ERROR_FAILURE; + } + + uint32_t len = gEventRecords->Length(); + for (uint32_t i = 0; i < len; ++i) { + const EventRecord& record = (*gEventRecords)[i]; + const EventInfo& info = gEventInfo[record.EventId()]; + + if (IsInDataset(info.common_info.dataset, aDataset)) { + events.AppendElement(record); + } + } + + if (aClear) { + gEventRecords->Clear(); + } + } + + // We serialize the events to a JS array. + JS::RootedObject 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]; + const EventInfo& info = gEventInfo[record.EventId()]; + + // 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::AutoValueVector 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. + const char* strings[] = { + info.common_info.category(), + info.method(), + info.object(), + }; + for (const char* s : strings) { + const NS_ConvertUTF8toUTF16 wide(s); + if (!items.append(JS::StringValue(JS_NewUCStringCopyN(cx, wide.Data(), wide.Length())))) { + return NS_ERROR_FAILURE; + } + } + + // Add the optional string value only when needed. + // When extra is empty and this has no value, we can save a little space. + if (record.Value()) { + const NS_ConvertUTF8toUTF16 wide(record.Value().value()); + if (!items.append(JS::StringValue(JS_NewUCStringCopyN(cx, wide.Data(), wide.Length())))) { + 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::RootedObject 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) { + const NS_ConvertUTF8toUTF16 wide(extra[i].value); + JS::Rooted<JS::Value> value(cx); + value.setString(JS_NewUCStringCopyN(cx, wide.Data(), wide.Length())); + + 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::RootedObject itemsArray(cx, JS_NewArrayObject(cx, items)); + if (!JS_DefineElement(cx, eventsArray, i, itemsArray, JSPROP_ENUMERATE)) { + return NS_ERROR_FAILURE; + } + } + + aResult.setObject(*eventsArray); + 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(); +} + +size_t +TelemetryEvent::SizeOfIncludingThis(mozilla::MallocSizeOf aMallocSizeOf) +{ + StaticMutexAutoLock locker(gTelemetryEventsMutex); + size_t n = 0; + + n += gEventRecords->ShallowSizeOfIncludingThis(aMallocSizeOf); + for (uint32_t i = 0; i < gEventRecords->Length(); ++i) { + n += (*gEventRecords)[i].SizeOfExcludingThis(aMallocSizeOf); + } + + n += gEventNameIDMap.ShallowSizeOfExcludingThis(aMallocSizeOf); + for (auto iter = gEventNameIDMap.ConstIter(); !iter.Done(); iter.Next()) { + n += iter.Key().SizeOfExcludingThisIfUnshared(aMallocSizeOf); + } + + return n; +} |