summaryrefslogtreecommitdiffstats
path: root/toolkit/components/telemetry/TelemetryEvent.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'toolkit/components/telemetry/TelemetryEvent.cpp')
-rw-r--r--toolkit/components/telemetry/TelemetryEvent.cpp687
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(&timestamp);
+ 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;
+}