/* -*- 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;
}
