1 /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
2 /* vim: set ts=8 sts=2 et sw=2 tw=80: */
3 /* This Source Code Form is subject to the terms of the Mozilla Public
4  * License, v. 2.0. If a copy of the MPL was not distributed with this
5  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
6 
7 #include "Telemetry.h"
8 #include "TelemetryEvent.h"
9 #include <prtime.h>
10 #include <limits>
11 #include "ipc/TelemetryIPCAccumulator.h"
12 #include "jsapi.h"
13 #include "js/Array.h"  // JS::GetArrayLength, JS::IsArrayObject, JS::NewArrayObject
14 #include "mozilla/Maybe.h"
15 #include "mozilla/Preferences.h"
16 #include "mozilla/Services.h"
17 #include "mozilla/StaticMutex.h"
18 #include "mozilla/StaticPtr.h"
19 #include "mozilla/Unused.h"
20 #include "nsClassHashtable.h"
21 #include "nsDataHashtable.h"
22 #include "nsHashKeys.h"
23 #include "nsIObserverService.h"
24 #include "nsITelemetry.h"
25 #include "nsJSUtils.h"
26 #include "nsPrintfCString.h"
27 #include "nsTArray.h"
28 #include "nsUTF8Utils.h"
29 #include "nsXULAppAPI.h"
30 #include "TelemetryCommon.h"
31 #include "TelemetryEventData.h"
32 #include "TelemetryScalar.h"
33 
34 using mozilla::Maybe;
35 using mozilla::StaticAutoPtr;
36 using mozilla::StaticMutex;
37 using mozilla::StaticMutexAutoLock;
38 using mozilla::TimeStamp;
39 using mozilla::Telemetry::ChildEventData;
40 using mozilla::Telemetry::EventExtraEntry;
41 using mozilla::Telemetry::LABELS_TELEMETRY_EVENT_RECORDING_ERROR;
42 using mozilla::Telemetry::LABELS_TELEMETRY_EVENT_REGISTRATION_ERROR;
43 using mozilla::Telemetry::ProcessID;
44 using mozilla::Telemetry::Common::CanRecordDataset;
45 using mozilla::Telemetry::Common::CanRecordInProcess;
46 using mozilla::Telemetry::Common::CanRecordProduct;
47 using mozilla::Telemetry::Common::GetNameForProcessID;
48 using mozilla::Telemetry::Common::IsExpiredVersion;
49 using mozilla::Telemetry::Common::IsInDataset;
50 using mozilla::Telemetry::Common::IsValidIdentifierString;
51 using mozilla::Telemetry::Common::LogToBrowserConsole;
52 using mozilla::Telemetry::Common::MsSinceProcessStart;
53 using mozilla::Telemetry::Common::ToJSString;
54 
55 namespace TelemetryIPCAccumulator = mozilla::TelemetryIPCAccumulator;
56 
57 ////////////////////////////////////////////////////////////////////////
58 ////////////////////////////////////////////////////////////////////////
59 //
60 // Naming: there are two kinds of functions in this file:
61 //
62 // * Functions taking a StaticMutexAutoLock: these can only be reached via
63 //   an interface function (TelemetryEvent::*). They expect the interface
64 //   function to have acquired |gTelemetryEventsMutex|, so they do not
65 //   have to be thread-safe.
66 //
67 // * Functions named TelemetryEvent::*. This is the external interface.
68 //   Entries and exits to these functions are serialised using
69 //   |gTelemetryEventsMutex|.
70 //
71 // Avoiding races and deadlocks:
72 //
73 // All functions in the external interface (TelemetryEvent::*) are
74 // serialised using the mutex |gTelemetryEventsMutex|. This means
75 // that the external interface is thread-safe, and the internal
76 // functions can ignore thread safety. But it also brings a danger
77 // of deadlock if any function in the external interface can get back
78 // to that interface. That is, we will deadlock on any call chain like
79 // this:
80 //
81 // TelemetryEvent::* -> .. any functions .. -> TelemetryEvent::*
82 //
83 // To reduce the danger of that happening, observe the following rules:
84 //
85 // * No function in TelemetryEvent::* may directly call, nor take the
86 //   address of, any other function in TelemetryEvent::*.
87 //
88 // * No internal function may call, nor take the address
89 //   of, any function in TelemetryEvent::*.
90 
91 ////////////////////////////////////////////////////////////////////////
92 ////////////////////////////////////////////////////////////////////////
93 //
94 // PRIVATE TYPES
95 
96 namespace {
97 
98 const uint32_t kEventCount =
99     static_cast<uint32_t>(mozilla::Telemetry::EventID::EventCount);
100 // This is a special event id used to mark expired events, to make expiry checks
101 // cheap at runtime.
102 const uint32_t kExpiredEventId = std::numeric_limits<uint32_t>::max();
103 static_assert(kExpiredEventId > kEventCount,
104               "Built-in event count should be less than the expired event id.");
105 
106 // Maximum length of any passed value string, in UTF8 byte sequence length.
107 const uint32_t kMaxValueByteLength = 80;
108 // Maximum length of any string value in the extra dictionary, in UTF8 byte
109 // sequence length.
110 const uint32_t kMaxExtraValueByteLength = 80;
111 // Maximum length of dynamic method names, in UTF8 byte sequence length.
112 const uint32_t kMaxMethodNameByteLength = 20;
113 // Maximum length of dynamic object names, in UTF8 byte sequence length.
114 const uint32_t kMaxObjectNameByteLength = 20;
115 // Maximum length of extra key names, in UTF8 byte sequence length.
116 const uint32_t kMaxExtraKeyNameByteLength = 15;
117 // The maximum number of valid extra keys for an event.
118 const uint32_t kMaxExtraKeyCount = 10;
119 
120 struct EventKey {
121   uint32_t id;
122   bool dynamic;
123 };
124 
125 struct DynamicEventInfo {
DynamicEventInfo__anon429f7d300111::DynamicEventInfo126   DynamicEventInfo(const nsACString& category, const nsACString& method,
127                    const nsACString& object, nsTArray<nsCString>& extra_keys,
128                    bool recordOnRelease, bool builtin)
129       : category(category),
130         method(method),
131         object(object),
132         extra_keys(extra_keys.Clone()),
133         recordOnRelease(recordOnRelease),
134         builtin(builtin) {}
135 
136   DynamicEventInfo(const DynamicEventInfo&) = default;
137   DynamicEventInfo& operator=(const DynamicEventInfo&) = delete;
138 
139   const nsCString category;
140   const nsCString method;
141   const nsCString object;
142   const CopyableTArray<nsCString> extra_keys;
143   const bool recordOnRelease;
144   const bool builtin;
145 
SizeOfExcludingThis__anon429f7d300111::DynamicEventInfo146   size_t SizeOfExcludingThis(mozilla::MallocSizeOf aMallocSizeOf) const {
147     size_t n = 0;
148 
149     n += category.SizeOfExcludingThisIfUnshared(aMallocSizeOf);
150     n += method.SizeOfExcludingThisIfUnshared(aMallocSizeOf);
151     n += object.SizeOfExcludingThisIfUnshared(aMallocSizeOf);
152     n += extra_keys.ShallowSizeOfExcludingThis(aMallocSizeOf);
153     for (auto& key : extra_keys) {
154       n += key.SizeOfExcludingThisIfUnshared(aMallocSizeOf);
155     }
156 
157     return n;
158   }
159 };
160 
161 enum class RecordEventResult {
162   Ok,
163   UnknownEvent,
164   InvalidExtraKey,
165   StorageLimitReached,
166   ExpiredEvent,
167   WrongProcess,
168 };
169 
170 typedef CopyableTArray<EventExtraEntry> ExtraArray;
171 
172 class EventRecord {
173  public:
EventRecord(double timestamp,const EventKey & key,const Maybe<nsCString> & value,const ExtraArray & extra)174   EventRecord(double timestamp, const EventKey& key,
175               const Maybe<nsCString>& value, const ExtraArray& extra)
176       : mTimestamp(timestamp),
177         mEventKey(key),
178         mValue(value),
179         mExtra(extra.Clone()) {}
180 
181   EventRecord(const EventRecord& other) = default;
182 
183   EventRecord& operator=(const EventRecord& other) = delete;
184 
Timestamp() const185   double Timestamp() const { return mTimestamp; }
GetEventKey() const186   const EventKey& GetEventKey() const { return mEventKey; }
Value() const187   const Maybe<nsCString>& Value() const { return mValue; }
Extra() const188   const ExtraArray& Extra() const { return mExtra; }
189 
190   size_t SizeOfExcludingThis(mozilla::MallocSizeOf aMallocSizeOf) const;
191 
192  private:
193   const double mTimestamp;
194   const EventKey mEventKey;
195   const Maybe<nsCString> mValue;
196   const ExtraArray mExtra;
197 };
198 
199 // Implements the methods for EventInfo.
method() const200 const nsDependentCString EventInfo::method() const {
201   return nsDependentCString(&gEventsStringTable[this->method_offset]);
202 }
203 
object() const204 const nsDependentCString EventInfo::object() const {
205   return nsDependentCString(&gEventsStringTable[this->object_offset]);
206 }
207 
208 // Implements the methods for CommonEventInfo.
category() const209 const nsDependentCString CommonEventInfo::category() const {
210   return nsDependentCString(&gEventsStringTable[this->category_offset]);
211 }
212 
expiration_version() const213 const nsDependentCString CommonEventInfo::expiration_version() const {
214   return nsDependentCString(
215       &gEventsStringTable[this->expiration_version_offset]);
216 }
217 
extra_key(uint32_t index) const218 const nsDependentCString CommonEventInfo::extra_key(uint32_t index) const {
219   MOZ_ASSERT(index < this->extra_count);
220   uint32_t key_index = gExtraKeysTable[this->extra_index + index];
221   return nsDependentCString(&gEventsStringTable[key_index]);
222 }
223 
224 // Implementation for the EventRecord class.
SizeOfExcludingThis(mozilla::MallocSizeOf aMallocSizeOf) const225 size_t EventRecord::SizeOfExcludingThis(
226     mozilla::MallocSizeOf aMallocSizeOf) const {
227   size_t n = 0;
228 
229   if (mValue) {
230     n += mValue.value().SizeOfExcludingThisIfUnshared(aMallocSizeOf);
231   }
232 
233   n += mExtra.ShallowSizeOfExcludingThis(aMallocSizeOf);
234   for (uint32_t i = 0; i < mExtra.Length(); ++i) {
235     n += mExtra[i].key.SizeOfExcludingThisIfUnshared(aMallocSizeOf);
236     n += mExtra[i].value.SizeOfExcludingThisIfUnshared(aMallocSizeOf);
237   }
238 
239   return n;
240 }
241 
UniqueEventName(const nsACString & category,const nsACString & method,const nsACString & object)242 nsCString UniqueEventName(const nsACString& category, const nsACString& method,
243                           const nsACString& object) {
244   nsCString name;
245   name.Append(category);
246   name.AppendLiteral("#");
247   name.Append(method);
248   name.AppendLiteral("#");
249   name.Append(object);
250   return name;
251 }
252 
UniqueEventName(const EventInfo & info)253 nsCString UniqueEventName(const EventInfo& info) {
254   return UniqueEventName(info.common_info.category(), info.method(),
255                          info.object());
256 }
257 
UniqueEventName(const DynamicEventInfo & info)258 nsCString UniqueEventName(const DynamicEventInfo& info) {
259   return UniqueEventName(info.category, info.method, info.object);
260 }
261 
TruncateToByteLength(nsCString & str,uint32_t length)262 void TruncateToByteLength(nsCString& str, uint32_t length) {
263   // last will be the index of the first byte of the current multi-byte
264   // sequence.
265   uint32_t last = RewindToPriorUTF8Codepoint(str.get(), length);
266   str.Truncate(last);
267 }
268 
269 }  // anonymous namespace
270 
271 ////////////////////////////////////////////////////////////////////////
272 ////////////////////////////////////////////////////////////////////////
273 //
274 // PRIVATE STATE, SHARED BY ALL THREADS
275 
276 namespace {
277 
278 // Set to true once this global state has been initialized.
279 bool gInitDone = false;
280 
281 bool gCanRecordBase;
282 bool gCanRecordExtended;
283 
284 // The EventName -> EventKey cache map.
285 nsClassHashtable<nsCStringHashKey, EventKey> gEventNameIDMap(kEventCount);
286 
287 // The CategoryName set.
288 nsTHashtable<nsCStringHashKey> gCategoryNames;
289 
290 // This tracks the IDs of the categories for which recording is enabled.
291 nsTHashtable<nsCStringHashKey> gEnabledCategories;
292 
293 // The main event storage. Events are inserted here, keyed by process id and
294 // in recording order.
295 typedef nsUint32HashKey ProcessIDHashKey;
296 typedef nsTArray<EventRecord> EventRecordArray;
297 typedef nsClassHashtable<ProcessIDHashKey, EventRecordArray>
298     EventRecordsMapType;
299 
300 EventRecordsMapType gEventRecords;
301 
302 // The details on dynamic events that are recorded from addons are registered
303 // here.
304 StaticAutoPtr<nsTArray<DynamicEventInfo>> gDynamicEventInfo;
305 
306 }  // namespace
307 
308 ////////////////////////////////////////////////////////////////////////
309 ////////////////////////////////////////////////////////////////////////
310 //
311 // PRIVATE: thread-safe helpers for event recording.
312 
313 namespace {
314 
GetDataset(const StaticMutexAutoLock & lock,const EventKey & eventKey)315 unsigned int GetDataset(const StaticMutexAutoLock& lock,
316                         const EventKey& eventKey) {
317   if (!eventKey.dynamic) {
318     return gEventInfo[eventKey.id].common_info.dataset;
319   }
320 
321   if (!gDynamicEventInfo) {
322     return nsITelemetry::DATASET_PRERELEASE_CHANNELS;
323   }
324 
325   return (*gDynamicEventInfo)[eventKey.id].recordOnRelease
326              ? nsITelemetry::DATASET_ALL_CHANNELS
327              : nsITelemetry::DATASET_PRERELEASE_CHANNELS;
328 }
329 
GetCategory(const StaticMutexAutoLock & lock,const EventKey & eventKey)330 nsCString GetCategory(const StaticMutexAutoLock& lock,
331                       const EventKey& eventKey) {
332   if (!eventKey.dynamic) {
333     return gEventInfo[eventKey.id].common_info.category();
334   }
335 
336   if (!gDynamicEventInfo) {
337     return NS_LITERAL_CSTRING("");
338   }
339 
340   return (*gDynamicEventInfo)[eventKey.id].category;
341 }
342 
CanRecordEvent(const StaticMutexAutoLock & lock,const EventKey & eventKey,ProcessID process)343 bool CanRecordEvent(const StaticMutexAutoLock& lock, const EventKey& eventKey,
344                     ProcessID process) {
345   if (!gCanRecordBase) {
346     return false;
347   }
348 
349   if (!CanRecordDataset(GetDataset(lock, eventKey), gCanRecordBase,
350                         gCanRecordExtended)) {
351     return false;
352   }
353 
354   // We don't allow specifying a process to record in for dynamic events.
355   if (!eventKey.dynamic) {
356     const CommonEventInfo& info = gEventInfo[eventKey.id].common_info;
357 
358     if (!CanRecordProduct(info.products)) {
359       return false;
360     }
361 
362     if (!CanRecordInProcess(info.record_in_processes, process)) {
363       return false;
364     }
365   }
366 
367   return true;
368 }
369 
IsExpired(const EventKey & key)370 bool IsExpired(const EventKey& key) { return key.id == kExpiredEventId; }
371 
GetEventRecordsForProcess(const StaticMutexAutoLock & lock,ProcessID processType)372 EventRecordArray* GetEventRecordsForProcess(const StaticMutexAutoLock& lock,
373                                             ProcessID processType) {
374   EventRecordArray* eventRecords = nullptr;
375   if (!gEventRecords.Get(uint32_t(processType), &eventRecords)) {
376     eventRecords = new EventRecordArray();
377     gEventRecords.Put(uint32_t(processType), eventRecords);
378   }
379   return eventRecords;
380 }
381 
GetEventKey(const StaticMutexAutoLock & lock,const nsACString & category,const nsACString & method,const nsACString & object)382 EventKey* GetEventKey(const StaticMutexAutoLock& lock,
383                       const nsACString& category, const nsACString& method,
384                       const nsACString& object) {
385   EventKey* event;
386   const nsCString& name = UniqueEventName(category, method, object);
387   if (!gEventNameIDMap.Get(name, &event)) {
388     return nullptr;
389   }
390   return event;
391 }
392 
CheckExtraKeysValid(const EventKey & eventKey,const ExtraArray & extra)393 static bool CheckExtraKeysValid(const EventKey& eventKey,
394                                 const ExtraArray& extra) {
395   nsTHashtable<nsCStringHashKey> validExtraKeys;
396   if (!eventKey.dynamic) {
397     const CommonEventInfo& common = gEventInfo[eventKey.id].common_info;
398     for (uint32_t i = 0; i < common.extra_count; ++i) {
399       validExtraKeys.PutEntry(common.extra_key(i));
400     }
401   } else if (gDynamicEventInfo) {
402     const DynamicEventInfo& info = (*gDynamicEventInfo)[eventKey.id];
403     for (uint32_t i = 0, len = info.extra_keys.Length(); i < len; ++i) {
404       validExtraKeys.PutEntry(info.extra_keys[i]);
405     }
406   }
407 
408   for (uint32_t i = 0; i < extra.Length(); ++i) {
409     if (!validExtraKeys.GetEntry(extra[i].key)) {
410       return false;
411     }
412   }
413 
414   return true;
415 }
416 
RecordEvent(const StaticMutexAutoLock & lock,ProcessID processType,double timestamp,const nsACString & category,const nsACString & method,const nsACString & object,const Maybe<nsCString> & value,const ExtraArray & extra)417 RecordEventResult RecordEvent(const StaticMutexAutoLock& lock,
418                               ProcessID processType, double timestamp,
419                               const nsACString& category,
420                               const nsACString& method,
421                               const nsACString& object,
422                               const Maybe<nsCString>& value,
423                               const ExtraArray& extra) {
424   // Look up the event id.
425   EventKey* eventKey = GetEventKey(lock, category, method, object);
426   if (!eventKey) {
427     mozilla::Telemetry::AccumulateCategorical(
428         LABELS_TELEMETRY_EVENT_RECORDING_ERROR::UnknownEvent);
429     return RecordEventResult::UnknownEvent;
430   }
431 
432   // If the event is expired or not enabled for this process, we silently drop
433   // this call. We don't want recording for expired probes to be an error so
434   // code doesn't have to be removed at a specific time or version. Even logging
435   // warnings would become very noisy.
436   if (IsExpired(*eventKey)) {
437     mozilla::Telemetry::AccumulateCategorical(
438         LABELS_TELEMETRY_EVENT_RECORDING_ERROR::Expired);
439     return RecordEventResult::ExpiredEvent;
440   }
441 
442   // Fixup the process id only for non-builtin (e.g. supporting build faster)
443   // dynamic events.
444   auto dynamicNonBuiltin =
445       eventKey->dynamic && !(*gDynamicEventInfo)[eventKey->id].builtin;
446   if (dynamicNonBuiltin) {
447     processType = ProcessID::Dynamic;
448   }
449 
450   // Check whether the extra keys passed are valid.
451   if (!CheckExtraKeysValid(*eventKey, extra)) {
452     mozilla::Telemetry::AccumulateCategorical(
453         LABELS_TELEMETRY_EVENT_RECORDING_ERROR::ExtraKey);
454     return RecordEventResult::InvalidExtraKey;
455   }
456 
457   // Check whether we can record this event.
458   if (!CanRecordEvent(lock, *eventKey, processType)) {
459     return RecordEventResult::Ok;
460   }
461 
462   // Count the number of times this event has been recorded, even if its
463   // category does not have recording enabled.
464   TelemetryScalar::SummarizeEvent(UniqueEventName(category, method, object),
465                                   processType, dynamicNonBuiltin);
466 
467   // Check whether this event's category has recording enabled
468   if (!gEnabledCategories.GetEntry(GetCategory(lock, *eventKey))) {
469     return RecordEventResult::Ok;
470   }
471 
472   static bool sEventPingEnabled = mozilla::Preferences::GetBool(
473       "toolkit.telemetry.eventping.enabled", true);
474   if (!sEventPingEnabled) {
475     return RecordEventResult::Ok;
476   }
477 
478   EventRecordArray* eventRecords = GetEventRecordsForProcess(lock, processType);
479   eventRecords->AppendElement(EventRecord(timestamp, *eventKey, value, extra));
480 
481   // Notify observers when we hit the "event" ping event record limit.
482   static uint32_t sEventPingLimit = mozilla::Preferences::GetUint(
483       "toolkit.telemetry.eventping.eventLimit", 1000);
484   if (eventRecords->Length() == sEventPingLimit) {
485     return RecordEventResult::StorageLimitReached;
486   }
487 
488   return RecordEventResult::Ok;
489 }
490 
ShouldRecordChildEvent(const StaticMutexAutoLock & lock,const nsACString & category,const nsACString & method,const nsACString & object)491 RecordEventResult ShouldRecordChildEvent(const StaticMutexAutoLock& lock,
492                                          const nsACString& category,
493                                          const nsACString& method,
494                                          const nsACString& object) {
495   EventKey* eventKey = GetEventKey(lock, category, method, object);
496   if (!eventKey) {
497     // This event is unknown in this process, but it might be a dynamic event
498     // that was registered in the parent process.
499     return RecordEventResult::Ok;
500   }
501 
502   if (IsExpired(*eventKey)) {
503     return RecordEventResult::ExpiredEvent;
504   }
505 
506   const auto processes =
507       gEventInfo[eventKey->id].common_info.record_in_processes;
508   if (!CanRecordInProcess(processes, XRE_GetProcessType())) {
509     return RecordEventResult::WrongProcess;
510   }
511 
512   return RecordEventResult::Ok;
513 }
514 
RegisterEvents(const StaticMutexAutoLock & lock,const nsACString & category,const nsTArray<DynamicEventInfo> & eventInfos,const nsTArray<bool> & eventExpired,bool aBuiltin)515 void RegisterEvents(const StaticMutexAutoLock& lock, const nsACString& category,
516                     const nsTArray<DynamicEventInfo>& eventInfos,
517                     const nsTArray<bool>& eventExpired, bool aBuiltin) {
518   MOZ_ASSERT(eventInfos.Length() == eventExpired.Length(),
519              "Event data array sizes should match.");
520 
521   // Register the new events.
522   if (!gDynamicEventInfo) {
523     gDynamicEventInfo = new nsTArray<DynamicEventInfo>();
524   }
525 
526   for (uint32_t i = 0, len = eventInfos.Length(); i < len; ++i) {
527     const nsCString& eventName = UniqueEventName(eventInfos[i]);
528 
529     // Re-registering events can happen for two reasons and we don't print
530     // warnings:
531     //
532     // * When add-ons update.
533     //   We don't support changing their definition, but the expiry might have
534     //   changed.
535     // * When dynamic builtins ("build faster") events are registered.
536     //   The dynamic definition takes precedence then.
537     EventKey* existing = nullptr;
538     if (!aBuiltin && gEventNameIDMap.Get(eventName, &existing)) {
539       if (eventExpired[i]) {
540         existing->id = kExpiredEventId;
541       }
542       continue;
543     }
544 
545     gDynamicEventInfo->AppendElement(eventInfos[i]);
546     uint32_t eventId =
547         eventExpired[i] ? kExpiredEventId : gDynamicEventInfo->Length() - 1;
548     gEventNameIDMap.Put(eventName, new EventKey{eventId, true});
549   }
550 
551   // If it is a builtin, add the category name in order to enable it later.
552   if (aBuiltin) {
553     gCategoryNames.PutEntry(category);
554   }
555 
556   if (!aBuiltin) {
557     // Now after successful registration enable recording for this category
558     // (if not a dynamic builtin).
559     gEnabledCategories.PutEntry(category);
560   }
561 }
562 
563 }  // anonymous namespace
564 
565 ////////////////////////////////////////////////////////////////////////
566 ////////////////////////////////////////////////////////////////////////
567 //
568 // PRIVATE: thread-unsafe helpers for event handling.
569 
570 namespace {
571 
SerializeEventsArray(const EventRecordArray & events,JSContext * cx,JS::MutableHandleObject result,unsigned int dataset)572 nsresult SerializeEventsArray(const EventRecordArray& events, JSContext* cx,
573                               JS::MutableHandleObject result,
574                               unsigned int dataset) {
575   // We serialize the events to a JS array.
576   JS::RootedObject eventsArray(cx, JS::NewArrayObject(cx, events.Length()));
577   if (!eventsArray) {
578     return NS_ERROR_FAILURE;
579   }
580 
581   for (uint32_t i = 0; i < events.Length(); ++i) {
582     const EventRecord& record = events[i];
583 
584     // Each entry is an array of one of the forms:
585     // [timestamp, category, method, object, value]
586     // [timestamp, category, method, object, null, extra]
587     // [timestamp, category, method, object, value, extra]
588     JS::RootedVector<JS::Value> items(cx);
589 
590     // Add timestamp.
591     JS::Rooted<JS::Value> val(cx);
592     if (!items.append(JS::NumberValue(floor(record.Timestamp())))) {
593       return NS_ERROR_FAILURE;
594     }
595 
596     // Add category, method, object.
597     auto addCategoryMethodObjectValues = [&](const nsACString& category,
598                                              const nsACString& method,
599                                              const nsACString& object) -> bool {
600       return items.append(JS::StringValue(ToJSString(cx, category))) &&
601              items.append(JS::StringValue(ToJSString(cx, method))) &&
602              items.append(JS::StringValue(ToJSString(cx, object)));
603     };
604 
605     const EventKey& eventKey = record.GetEventKey();
606     if (!eventKey.dynamic) {
607       const EventInfo& info = gEventInfo[eventKey.id];
608       if (!addCategoryMethodObjectValues(info.common_info.category(),
609                                          info.method(), info.object())) {
610         return NS_ERROR_FAILURE;
611       }
612     } else if (gDynamicEventInfo) {
613       const DynamicEventInfo& info = (*gDynamicEventInfo)[eventKey.id];
614       if (!addCategoryMethodObjectValues(info.category, info.method,
615                                          info.object)) {
616         return NS_ERROR_FAILURE;
617       }
618     }
619 
620     // Add the optional string value only when needed.
621     // When the value field is empty and extra is not set, we can save a little
622     // space that way. We still need to submit a null value if extra is set, to
623     // match the form: [ts, category, method, object, null, extra]
624     if (record.Value()) {
625       if (!items.append(
626               JS::StringValue(ToJSString(cx, record.Value().value())))) {
627         return NS_ERROR_FAILURE;
628       }
629     } else if (!record.Extra().IsEmpty()) {
630       if (!items.append(JS::NullValue())) {
631         return NS_ERROR_FAILURE;
632       }
633     }
634 
635     // Add the optional extra dictionary.
636     // To save a little space, only add it when it is not empty.
637     if (!record.Extra().IsEmpty()) {
638       JS::RootedObject obj(cx, JS_NewPlainObject(cx));
639       if (!obj) {
640         return NS_ERROR_FAILURE;
641       }
642 
643       // Add extra key & value entries.
644       const ExtraArray& extra = record.Extra();
645       for (uint32_t i = 0; i < extra.Length(); ++i) {
646         JS::Rooted<JS::Value> value(cx);
647         value.setString(ToJSString(cx, extra[i].value));
648 
649         if (!JS_DefineProperty(cx, obj, extra[i].key.get(), value,
650                                JSPROP_ENUMERATE)) {
651           return NS_ERROR_FAILURE;
652         }
653       }
654       val.setObject(*obj);
655 
656       if (!items.append(val)) {
657         return NS_ERROR_FAILURE;
658       }
659     }
660 
661     // Add the record to the events array.
662     JS::RootedObject itemsArray(cx, JS::NewArrayObject(cx, items));
663     if (!JS_DefineElement(cx, eventsArray, i, itemsArray, JSPROP_ENUMERATE)) {
664       return NS_ERROR_FAILURE;
665     }
666   }
667 
668   result.set(eventsArray);
669   return NS_OK;
670 }
671 
672 }  // anonymous namespace
673 
674 ////////////////////////////////////////////////////////////////////////
675 ////////////////////////////////////////////////////////////////////////
676 //
677 // EXTERNALLY VISIBLE FUNCTIONS in namespace TelemetryEvents::
678 
679 // This is a StaticMutex rather than a plain Mutex (1) so that
680 // it gets initialised in a thread-safe manner the first time
681 // it is used, and (2) because it is never de-initialised, and
682 // a normal Mutex would show up as a leak in BloatView.  StaticMutex
683 // also has the "OffTheBooks" property, so it won't show as a leak
684 // in BloatView.
685 // Another reason to use a StaticMutex instead of a plain Mutex is
686 // that, due to the nature of Telemetry, we cannot rely on having a
687 // mutex initialized in InitializeGlobalState. Unfortunately, we
688 // cannot make sure that no other function is called before this point.
689 static StaticMutex gTelemetryEventsMutex;
690 
InitializeGlobalState(bool aCanRecordBase,bool aCanRecordExtended)691 void TelemetryEvent::InitializeGlobalState(bool aCanRecordBase,
692                                            bool aCanRecordExtended) {
693   StaticMutexAutoLock locker(gTelemetryEventsMutex);
694   MOZ_ASSERT(!gInitDone,
695              "TelemetryEvent::InitializeGlobalState "
696              "may only be called once");
697 
698   gCanRecordBase = aCanRecordBase;
699   gCanRecordExtended = aCanRecordExtended;
700 
701   // Populate the static event name->id cache. Note that the event names are
702   // statically allocated and come from the automatically generated
703   // TelemetryEventData.h.
704   const uint32_t eventCount =
705       static_cast<uint32_t>(mozilla::Telemetry::EventID::EventCount);
706   for (uint32_t i = 0; i < eventCount; ++i) {
707     const EventInfo& info = gEventInfo[i];
708     uint32_t eventId = i;
709 
710     // If this event is expired or not recorded in this process, mark it with
711     // a special event id.
712     // This avoids doing repeated checks at runtime.
713     if (IsExpiredVersion(info.common_info.expiration_version().get())) {
714       eventId = kExpiredEventId;
715     }
716 
717     gEventNameIDMap.Put(UniqueEventName(info), new EventKey{eventId, false});
718     gCategoryNames.PutEntry(info.common_info.category());
719   }
720 
721   gInitDone = true;
722 }
723 
DeInitializeGlobalState()724 void TelemetryEvent::DeInitializeGlobalState() {
725   StaticMutexAutoLock locker(gTelemetryEventsMutex);
726   MOZ_ASSERT(gInitDone);
727 
728   gCanRecordBase = false;
729   gCanRecordExtended = false;
730 
731   gEventNameIDMap.Clear();
732   gCategoryNames.Clear();
733   gEnabledCategories.Clear();
734   gEventRecords.Clear();
735 
736   gDynamicEventInfo = nullptr;
737 
738   gInitDone = false;
739 }
740 
SetCanRecordBase(bool b)741 void TelemetryEvent::SetCanRecordBase(bool b) {
742   StaticMutexAutoLock locker(gTelemetryEventsMutex);
743   gCanRecordBase = b;
744 }
745 
SetCanRecordExtended(bool b)746 void TelemetryEvent::SetCanRecordExtended(bool b) {
747   StaticMutexAutoLock locker(gTelemetryEventsMutex);
748   gCanRecordExtended = b;
749 }
750 
RecordChildEvents(ProcessID aProcessType,const nsTArray<mozilla::Telemetry::ChildEventData> & aEvents)751 nsresult TelemetryEvent::RecordChildEvents(
752     ProcessID aProcessType,
753     const nsTArray<mozilla::Telemetry::ChildEventData>& aEvents) {
754   MOZ_ASSERT(XRE_IsParentProcess());
755   StaticMutexAutoLock locker(gTelemetryEventsMutex);
756   for (uint32_t i = 0; i < aEvents.Length(); ++i) {
757     const mozilla::Telemetry::ChildEventData& e = aEvents[i];
758 
759     // Timestamps from child processes are absolute. We fix them up here to be
760     // relative to the main process start time.
761     // This allows us to put events from all processes on the same timeline.
762     double relativeTimestamp =
763         (e.timestamp - TimeStamp::ProcessCreation()).ToMilliseconds();
764 
765     ::RecordEvent(locker, aProcessType, relativeTimestamp, e.category, e.method,
766                   e.object, e.value, e.extra);
767   }
768   return NS_OK;
769 }
770 
RecordEvent(const nsACString & aCategory,const nsACString & aMethod,const nsACString & aObject,JS::HandleValue aValue,JS::HandleValue aExtra,JSContext * cx,uint8_t optional_argc)771 nsresult TelemetryEvent::RecordEvent(const nsACString& aCategory,
772                                      const nsACString& aMethod,
773                                      const nsACString& aObject,
774                                      JS::HandleValue aValue,
775                                      JS::HandleValue aExtra, JSContext* cx,
776                                      uint8_t optional_argc) {
777   // Check value argument.
778   if ((optional_argc > 0) && !aValue.isNull() && !aValue.isString()) {
779     LogToBrowserConsole(nsIScriptError::warningFlag,
780                         NS_LITERAL_STRING("Invalid type for value parameter."));
781     mozilla::Telemetry::AccumulateCategorical(
782         LABELS_TELEMETRY_EVENT_RECORDING_ERROR::Value);
783     return NS_OK;
784   }
785 
786   // Extract value parameter.
787   Maybe<nsCString> value;
788   if (aValue.isString()) {
789     nsAutoJSString jsStr;
790     if (!jsStr.init(cx, aValue)) {
791       LogToBrowserConsole(
792           nsIScriptError::warningFlag,
793           NS_LITERAL_STRING("Invalid string value for value parameter."));
794       mozilla::Telemetry::AccumulateCategorical(
795           LABELS_TELEMETRY_EVENT_RECORDING_ERROR::Value);
796       return NS_OK;
797     }
798 
799     nsCString str = NS_ConvertUTF16toUTF8(jsStr);
800     if (str.Length() > kMaxValueByteLength) {
801       LogToBrowserConsole(
802           nsIScriptError::warningFlag,
803           NS_LITERAL_STRING(
804               "Value parameter exceeds maximum string length, truncating."));
805       TruncateToByteLength(str, kMaxValueByteLength);
806     }
807     value = mozilla::Some(str);
808   }
809 
810   // Check extra argument.
811   if ((optional_argc > 1) && !aExtra.isNull() && !aExtra.isObject()) {
812     LogToBrowserConsole(nsIScriptError::warningFlag,
813                         NS_LITERAL_STRING("Invalid type for extra parameter."));
814     mozilla::Telemetry::AccumulateCategorical(
815         LABELS_TELEMETRY_EVENT_RECORDING_ERROR::Extra);
816     return NS_OK;
817   }
818 
819   // Extract extra dictionary.
820   ExtraArray extra;
821   if (aExtra.isObject()) {
822     JS::RootedObject obj(cx, &aExtra.toObject());
823     JS::Rooted<JS::IdVector> ids(cx, JS::IdVector(cx));
824     if (!JS_Enumerate(cx, obj, &ids)) {
825       LogToBrowserConsole(nsIScriptError::warningFlag,
826                           NS_LITERAL_STRING("Failed to enumerate object."));
827       mozilla::Telemetry::AccumulateCategorical(
828           LABELS_TELEMETRY_EVENT_RECORDING_ERROR::Extra);
829       return NS_OK;
830     }
831 
832     for (size_t i = 0, n = ids.length(); i < n; i++) {
833       nsAutoJSString key;
834       if (!key.init(cx, ids[i])) {
835         LogToBrowserConsole(
836             nsIScriptError::warningFlag,
837             NS_LITERAL_STRING(
838                 "Extra dictionary should only contain string keys."));
839         mozilla::Telemetry::AccumulateCategorical(
840             LABELS_TELEMETRY_EVENT_RECORDING_ERROR::Extra);
841         return NS_OK;
842       }
843 
844       JS::Rooted<JS::Value> value(cx);
845       if (!JS_GetPropertyById(cx, obj, ids[i], &value)) {
846         LogToBrowserConsole(nsIScriptError::warningFlag,
847                             NS_LITERAL_STRING("Failed to get extra property."));
848         mozilla::Telemetry::AccumulateCategorical(
849             LABELS_TELEMETRY_EVENT_RECORDING_ERROR::Extra);
850         return NS_OK;
851       }
852 
853       nsAutoJSString jsStr;
854       if (!value.isString() || !jsStr.init(cx, value)) {
855         LogToBrowserConsole(
856             nsIScriptError::warningFlag,
857             NS_LITERAL_STRING("Extra properties should have string values."));
858         mozilla::Telemetry::AccumulateCategorical(
859             LABELS_TELEMETRY_EVENT_RECORDING_ERROR::Extra);
860         return NS_OK;
861       }
862 
863       nsCString str = NS_ConvertUTF16toUTF8(jsStr);
864       if (str.Length() > kMaxExtraValueByteLength) {
865         LogToBrowserConsole(
866             nsIScriptError::warningFlag,
867             NS_LITERAL_STRING(
868                 "Extra value exceeds maximum string length, truncating."));
869         TruncateToByteLength(str, kMaxExtraValueByteLength);
870       }
871 
872       extra.AppendElement(EventExtraEntry{NS_ConvertUTF16toUTF8(key), str});
873     }
874   }
875 
876   // Lock for accessing internal data.
877   // While the lock is being held, no complex calls like JS calls can be made,
878   // as all of these could record Telemetry, which would result in deadlock.
879   RecordEventResult res;
880   if (!XRE_IsParentProcess()) {
881     {
882       StaticMutexAutoLock lock(gTelemetryEventsMutex);
883       res = ::ShouldRecordChildEvent(lock, aCategory, aMethod, aObject);
884     }
885 
886     if (res == RecordEventResult::Ok) {
887       TelemetryIPCAccumulator::RecordChildEvent(
888           TimeStamp::NowLoRes(), aCategory, aMethod, aObject, value, extra);
889     }
890   } else {
891     StaticMutexAutoLock lock(gTelemetryEventsMutex);
892 
893     if (!gInitDone) {
894       return NS_ERROR_FAILURE;
895     }
896 
897     // Get the current time.
898     double timestamp = -1;
899     if (NS_WARN_IF(NS_FAILED(MsSinceProcessStart(&timestamp)))) {
900       return NS_ERROR_FAILURE;
901     }
902 
903     res = ::RecordEvent(lock, ProcessID::Parent, timestamp, aCategory, aMethod,
904                         aObject, value, extra);
905   }
906 
907   // Trigger warnings or errors where needed.
908   switch (res) {
909     case RecordEventResult::UnknownEvent: {
910       nsPrintfCString msg(R"(Unknown event: ["%s", "%s", "%s"])",
911                           PromiseFlatCString(aCategory).get(),
912                           PromiseFlatCString(aMethod).get(),
913                           PromiseFlatCString(aObject).get());
914       LogToBrowserConsole(nsIScriptError::errorFlag,
915                           NS_ConvertUTF8toUTF16(msg));
916       return NS_OK;
917     }
918     case RecordEventResult::InvalidExtraKey: {
919       nsPrintfCString msg(R"(Invalid extra key for event ["%s", "%s", "%s"].)",
920                           PromiseFlatCString(aCategory).get(),
921                           PromiseFlatCString(aMethod).get(),
922                           PromiseFlatCString(aObject).get());
923       LogToBrowserConsole(nsIScriptError::warningFlag,
924                           NS_ConvertUTF8toUTF16(msg));
925       return NS_OK;
926     }
927     case RecordEventResult::StorageLimitReached: {
928       LogToBrowserConsole(nsIScriptError::warningFlag,
929                           NS_LITERAL_STRING("Event storage limit reached."));
930       nsCOMPtr<nsIObserverService> serv =
931           mozilla::services::GetObserverService();
932       if (serv) {
933         serv->NotifyObservers(nullptr, "event-telemetry-storage-limit-reached",
934                               nullptr);
935       }
936       return NS_OK;
937     }
938     default:
939       return NS_OK;
940   }
941 }
942 
RecordEventNative(mozilla::Telemetry::EventID aId,const mozilla::Maybe<nsCString> & aValue,const mozilla::Maybe<ExtraArray> & aExtra)943 void TelemetryEvent::RecordEventNative(
944     mozilla::Telemetry::EventID aId, const mozilla::Maybe<nsCString>& aValue,
945     const mozilla::Maybe<ExtraArray>& aExtra) {
946   // Truncate aValue if present and necessary.
947   mozilla::Maybe<nsCString> value;
948   if (aValue) {
949     nsCString valueStr = aValue.ref();
950     if (valueStr.Length() > kMaxValueByteLength) {
951       TruncateToByteLength(valueStr, kMaxValueByteLength);
952     }
953     value = mozilla::Some(valueStr);
954   }
955 
956   // Truncate any over-long extra values.
957   ExtraArray extra;
958   if (aExtra) {
959     extra = aExtra.value();
960     for (auto& item : extra) {
961       if (item.value.Length() > kMaxExtraValueByteLength) {
962         TruncateToByteLength(item.value, kMaxExtraValueByteLength);
963       }
964     }
965   }
966 
967   const EventInfo& info = gEventInfo[static_cast<uint32_t>(aId)];
968   const nsCString category(info.common_info.category());
969   const nsCString method(info.method());
970   const nsCString object(info.object());
971   if (!XRE_IsParentProcess()) {
972     RecordEventResult res;
973     {
974       StaticMutexAutoLock lock(gTelemetryEventsMutex);
975       res = ::ShouldRecordChildEvent(lock, category, method, object);
976     }
977 
978     if (res == RecordEventResult::Ok) {
979       TelemetryIPCAccumulator::RecordChildEvent(TimeStamp::NowLoRes(), category,
980                                                 method, object, value, extra);
981     }
982   } else {
983     StaticMutexAutoLock lock(gTelemetryEventsMutex);
984 
985     if (!gInitDone) {
986       return;
987     }
988 
989     // Get the current time.
990     double timestamp = -1;
991     if (NS_WARN_IF(NS_FAILED(MsSinceProcessStart(&timestamp)))) {
992       return;
993     }
994 
995     ::RecordEvent(lock, ProcessID::Parent, timestamp, category, method, object,
996                   value, extra);
997   }
998 }
999 
GetArrayPropertyValues(JSContext * cx,JS::HandleObject obj,const char * property,nsTArray<nsCString> * results)1000 static bool GetArrayPropertyValues(JSContext* cx, JS::HandleObject obj,
1001                                    const char* property,
1002                                    nsTArray<nsCString>* results) {
1003   JS::RootedValue value(cx);
1004   if (!JS_GetProperty(cx, obj, property, &value)) {
1005     JS_ReportErrorASCII(cx, R"(Missing required property "%s" for event)",
1006                         property);
1007     return false;
1008   }
1009 
1010   bool isArray = false;
1011   if (!JS::IsArrayObject(cx, value, &isArray) || !isArray) {
1012     JS_ReportErrorASCII(cx, R"(Property "%s" for event should be an array)",
1013                         property);
1014     return false;
1015   }
1016 
1017   JS::RootedObject arrayObj(cx, &value.toObject());
1018   uint32_t arrayLength;
1019   if (!JS::GetArrayLength(cx, arrayObj, &arrayLength)) {
1020     return false;
1021   }
1022 
1023   for (uint32_t arrayIdx = 0; arrayIdx < arrayLength; ++arrayIdx) {
1024     JS::Rooted<JS::Value> element(cx);
1025     if (!JS_GetElement(cx, arrayObj, arrayIdx, &element)) {
1026       return false;
1027     }
1028 
1029     if (!element.isString()) {
1030       JS_ReportErrorASCII(
1031           cx, R"(Array entries for event property "%s" should be strings)",
1032           property);
1033       return false;
1034     }
1035 
1036     nsAutoJSString jsStr;
1037     if (!jsStr.init(cx, element)) {
1038       return false;
1039     }
1040 
1041     results->AppendElement(NS_ConvertUTF16toUTF8(jsStr));
1042   }
1043 
1044   return true;
1045 }
1046 
RegisterEvents(const nsACString & aCategory,JS::Handle<JS::Value> aEventData,bool aBuiltin,JSContext * cx)1047 nsresult TelemetryEvent::RegisterEvents(const nsACString& aCategory,
1048                                         JS::Handle<JS::Value> aEventData,
1049                                         bool aBuiltin, JSContext* cx) {
1050   MOZ_ASSERT(XRE_IsParentProcess(),
1051              "Events can only be registered in the parent process");
1052 
1053   if (!IsValidIdentifierString(aCategory, 30, true, true)) {
1054     JS_ReportErrorASCII(
1055         cx, "Category parameter should match the identifier pattern.");
1056     mozilla::Telemetry::AccumulateCategorical(
1057         LABELS_TELEMETRY_EVENT_REGISTRATION_ERROR::Category);
1058     return NS_ERROR_INVALID_ARG;
1059   }
1060 
1061   if (!aEventData.isObject()) {
1062     JS_ReportErrorASCII(cx, "Event data parameter should be an object");
1063     mozilla::Telemetry::AccumulateCategorical(
1064         LABELS_TELEMETRY_EVENT_REGISTRATION_ERROR::Other);
1065     return NS_ERROR_INVALID_ARG;
1066   }
1067 
1068   JS::RootedObject obj(cx, &aEventData.toObject());
1069   JS::Rooted<JS::IdVector> eventPropertyIds(cx, JS::IdVector(cx));
1070   if (!JS_Enumerate(cx, obj, &eventPropertyIds)) {
1071     mozilla::Telemetry::AccumulateCategorical(
1072         LABELS_TELEMETRY_EVENT_REGISTRATION_ERROR::Other);
1073     return NS_ERROR_FAILURE;
1074   }
1075 
1076   // Collect the event data into local storage first.
1077   // Only after successfully validating all contained events will we register
1078   // them into global storage.
1079   nsTArray<DynamicEventInfo> newEventInfos;
1080   nsTArray<bool> newEventExpired;
1081 
1082   for (size_t i = 0, n = eventPropertyIds.length(); i < n; i++) {
1083     nsAutoJSString eventName;
1084     if (!eventName.init(cx, eventPropertyIds[i])) {
1085       mozilla::Telemetry::AccumulateCategorical(
1086           LABELS_TELEMETRY_EVENT_REGISTRATION_ERROR::Other);
1087       return NS_ERROR_FAILURE;
1088     }
1089 
1090     if (!IsValidIdentifierString(NS_ConvertUTF16toUTF8(eventName),
1091                                  kMaxMethodNameByteLength, false, true)) {
1092       JS_ReportErrorASCII(cx,
1093                           "Event names should match the identifier pattern.");
1094       mozilla::Telemetry::AccumulateCategorical(
1095           LABELS_TELEMETRY_EVENT_REGISTRATION_ERROR::Name);
1096       return NS_ERROR_INVALID_ARG;
1097     }
1098 
1099     JS::RootedValue value(cx);
1100     if (!JS_GetPropertyById(cx, obj, eventPropertyIds[i], &value) ||
1101         !value.isObject()) {
1102       mozilla::Telemetry::AccumulateCategorical(
1103           LABELS_TELEMETRY_EVENT_REGISTRATION_ERROR::Other);
1104       return NS_ERROR_FAILURE;
1105     }
1106     JS::RootedObject eventObj(cx, &value.toObject());
1107 
1108     // Extract the event registration data.
1109     nsTArray<nsCString> methods;
1110     nsTArray<nsCString> objects;
1111     nsTArray<nsCString> extra_keys;
1112     bool expired = false;
1113     bool recordOnRelease = false;
1114 
1115     // The methods & objects properties are required.
1116     if (!GetArrayPropertyValues(cx, eventObj, "methods", &methods)) {
1117       mozilla::Telemetry::AccumulateCategorical(
1118           LABELS_TELEMETRY_EVENT_REGISTRATION_ERROR::Other);
1119       return NS_ERROR_FAILURE;
1120     }
1121 
1122     if (!GetArrayPropertyValues(cx, eventObj, "objects", &objects)) {
1123       mozilla::Telemetry::AccumulateCategorical(
1124           LABELS_TELEMETRY_EVENT_REGISTRATION_ERROR::Other);
1125       return NS_ERROR_FAILURE;
1126     }
1127 
1128     // extra_keys is optional.
1129     bool hasProperty = false;
1130     if (JS_HasProperty(cx, eventObj, "extra_keys", &hasProperty) &&
1131         hasProperty) {
1132       if (!GetArrayPropertyValues(cx, eventObj, "extra_keys", &extra_keys)) {
1133         mozilla::Telemetry::AccumulateCategorical(
1134             LABELS_TELEMETRY_EVENT_REGISTRATION_ERROR::Other);
1135         return NS_ERROR_FAILURE;
1136       }
1137     }
1138 
1139     // expired is optional.
1140     if (JS_HasProperty(cx, eventObj, "expired", &hasProperty) && hasProperty) {
1141       JS::RootedValue temp(cx);
1142       if (!JS_GetProperty(cx, eventObj, "expired", &temp) ||
1143           !temp.isBoolean()) {
1144         mozilla::Telemetry::AccumulateCategorical(
1145             LABELS_TELEMETRY_EVENT_REGISTRATION_ERROR::Other);
1146         return NS_ERROR_FAILURE;
1147       }
1148 
1149       expired = temp.toBoolean();
1150     }
1151 
1152     // record_on_release is optional.
1153     if (JS_HasProperty(cx, eventObj, "record_on_release", &hasProperty) &&
1154         hasProperty) {
1155       JS::RootedValue temp(cx);
1156       if (!JS_GetProperty(cx, eventObj, "record_on_release", &temp) ||
1157           !temp.isBoolean()) {
1158         mozilla::Telemetry::AccumulateCategorical(
1159             LABELS_TELEMETRY_EVENT_REGISTRATION_ERROR::Other);
1160         return NS_ERROR_FAILURE;
1161       }
1162 
1163       recordOnRelease = temp.toBoolean();
1164     }
1165 
1166     // Validate methods.
1167     for (auto& method : methods) {
1168       if (!IsValidIdentifierString(method, kMaxMethodNameByteLength, false,
1169                                    true)) {
1170         JS_ReportErrorASCII(
1171             cx, "Method names should match the identifier pattern.");
1172         mozilla::Telemetry::AccumulateCategorical(
1173             LABELS_TELEMETRY_EVENT_REGISTRATION_ERROR::Method);
1174         return NS_ERROR_INVALID_ARG;
1175       }
1176     }
1177 
1178     // Validate objects.
1179     for (auto& object : objects) {
1180       if (!IsValidIdentifierString(object, kMaxObjectNameByteLength, false,
1181                                    true)) {
1182         JS_ReportErrorASCII(
1183             cx, "Object names should match the identifier pattern.");
1184         mozilla::Telemetry::AccumulateCategorical(
1185             LABELS_TELEMETRY_EVENT_REGISTRATION_ERROR::Object);
1186         return NS_ERROR_INVALID_ARG;
1187       }
1188     }
1189 
1190     // Validate extra keys.
1191     if (extra_keys.Length() > kMaxExtraKeyCount) {
1192       JS_ReportErrorASCII(cx, "No more than 10 extra keys can be registered.");
1193       mozilla::Telemetry::AccumulateCategorical(
1194           LABELS_TELEMETRY_EVENT_REGISTRATION_ERROR::ExtraKeys);
1195       return NS_ERROR_INVALID_ARG;
1196     }
1197     for (auto& key : extra_keys) {
1198       if (!IsValidIdentifierString(key, kMaxExtraKeyNameByteLength, false,
1199                                    true)) {
1200         JS_ReportErrorASCII(
1201             cx, "Extra key names should match the identifier pattern.");
1202         mozilla::Telemetry::AccumulateCategorical(
1203             LABELS_TELEMETRY_EVENT_REGISTRATION_ERROR::ExtraKeys);
1204         return NS_ERROR_INVALID_ARG;
1205       }
1206     }
1207 
1208     // Append event infos to be registered.
1209     for (auto& method : methods) {
1210       for (auto& object : objects) {
1211         // We defer the actual registration here in case any other event
1212         // description is invalid. In that case we don't need to roll back any
1213         // partial registration.
1214         DynamicEventInfo info{aCategory,  method,          object,
1215                               extra_keys, recordOnRelease, aBuiltin};
1216         newEventInfos.AppendElement(info);
1217         newEventExpired.AppendElement(expired);
1218       }
1219     }
1220   }
1221 
1222   {
1223     StaticMutexAutoLock locker(gTelemetryEventsMutex);
1224     RegisterEvents(locker, aCategory, newEventInfos, newEventExpired, aBuiltin);
1225   }
1226 
1227   return NS_OK;
1228 }
1229 
CreateSnapshots(uint32_t aDataset,bool aClear,uint32_t aEventLimit,JSContext * cx,uint8_t optional_argc,JS::MutableHandleValue aResult)1230 nsresult TelemetryEvent::CreateSnapshots(uint32_t aDataset, bool aClear,
1231                                          uint32_t aEventLimit, JSContext* cx,
1232                                          uint8_t optional_argc,
1233                                          JS::MutableHandleValue aResult) {
1234   if (!XRE_IsParentProcess()) {
1235     return NS_ERROR_FAILURE;
1236   }
1237 
1238   // Creating a JS snapshot of the events is a two-step process:
1239   // (1) Lock the storage and copy the events into function-local storage.
1240   // (2) Serialize the events into JS.
1241   // We can't hold a lock for (2) because we will run into deadlocks otherwise
1242   // from JS recording Telemetry.
1243 
1244   // (1) Extract the events from storage with a lock held.
1245   nsTArray<std::pair<const char*, EventRecordArray>> processEvents;
1246   nsTArray<std::pair<uint32_t, EventRecordArray>> leftovers;
1247   {
1248     StaticMutexAutoLock locker(gTelemetryEventsMutex);
1249 
1250     if (!gInitDone) {
1251       return NS_ERROR_FAILURE;
1252     }
1253 
1254     // The snapshotting function is the same for both static and dynamic builtin
1255     // events. We can use the same function and store the events in the same
1256     // output storage.
1257     auto snapshotter = [aDataset, &locker, &processEvents, &leftovers, aClear,
1258                         optional_argc,
1259                         aEventLimit](EventRecordsMapType& aProcessStorage) {
1260       for (auto iter = aProcessStorage.Iter(); !iter.Done(); iter.Next()) {
1261         const EventRecordArray* eventStorage = iter.UserData();
1262         EventRecordArray events;
1263         EventRecordArray leftoverEvents;
1264 
1265         const uint32_t len = eventStorage->Length();
1266         for (uint32_t i = 0; i < len; ++i) {
1267           const EventRecord& record = (*eventStorage)[i];
1268           if (IsInDataset(GetDataset(locker, record.GetEventKey()), aDataset)) {
1269             // If we have a limit, adhere to it. If we have a limit and are
1270             // going to clear, save the leftovers for later.
1271             if (optional_argc < 2 || events.Length() < aEventLimit) {
1272               events.AppendElement(record);
1273             } else if (aClear) {
1274               leftoverEvents.AppendElement(record);
1275             }
1276           }
1277         }
1278 
1279         if (events.Length()) {
1280           const char* processName = GetNameForProcessID(ProcessID(iter.Key()));
1281           processEvents.AppendElement(
1282               std::make_pair(processName, std::move(events)));
1283           if (leftoverEvents.Length()) {
1284             leftovers.AppendElement(
1285                 std::make_pair(iter.Key(), std::move(leftoverEvents)));
1286           }
1287         }
1288       }
1289     };
1290 
1291     // Take a snapshot of the plain and dynamic builtin events.
1292     snapshotter(gEventRecords);
1293     if (aClear) {
1294       gEventRecords.Clear();
1295       for (auto& pair : leftovers) {
1296         gEventRecords.Put(pair.first,
1297                           new EventRecordArray(std::move(pair.second)));
1298       }
1299       leftovers.Clear();
1300     }
1301   }
1302 
1303   // (2) Serialize the events to a JS object.
1304   JS::RootedObject rootObj(cx, JS_NewPlainObject(cx));
1305   if (!rootObj) {
1306     return NS_ERROR_FAILURE;
1307   }
1308 
1309   const uint32_t processLength = processEvents.Length();
1310   for (uint32_t i = 0; i < processLength; ++i) {
1311     JS::RootedObject eventsArray(cx);
1312     if (NS_FAILED(SerializeEventsArray(processEvents[i].second, cx,
1313                                        &eventsArray, aDataset))) {
1314       return NS_ERROR_FAILURE;
1315     }
1316 
1317     if (!JS_DefineProperty(cx, rootObj, processEvents[i].first, eventsArray,
1318                            JSPROP_ENUMERATE)) {
1319       return NS_ERROR_FAILURE;
1320     }
1321   }
1322 
1323   aResult.setObject(*rootObj);
1324   return NS_OK;
1325 }
1326 
1327 /**
1328  * Resets all the stored events. This is intended to be only used in tests.
1329  */
ClearEvents()1330 void TelemetryEvent::ClearEvents() {
1331   StaticMutexAutoLock lock(gTelemetryEventsMutex);
1332 
1333   if (!gInitDone) {
1334     return;
1335   }
1336 
1337   gEventRecords.Clear();
1338 }
1339 
SetEventRecordingEnabled(const nsACString & category,bool enabled)1340 void TelemetryEvent::SetEventRecordingEnabled(const nsACString& category,
1341                                               bool enabled) {
1342   StaticMutexAutoLock locker(gTelemetryEventsMutex);
1343 
1344   if (!gCategoryNames.Contains(category)) {
1345     LogToBrowserConsole(
1346         nsIScriptError::warningFlag,
1347         NS_ConvertUTF8toUTF16(
1348             NS_LITERAL_CSTRING(
1349                 "Unknown category for SetEventRecordingEnabled: ") +
1350             category));
1351     return;
1352   }
1353 
1354   if (enabled) {
1355     gEnabledCategories.PutEntry(category);
1356   } else {
1357     gEnabledCategories.RemoveEntry(category);
1358   }
1359 }
1360 
SizeOfIncludingThis(mozilla::MallocSizeOf aMallocSizeOf)1361 size_t TelemetryEvent::SizeOfIncludingThis(
1362     mozilla::MallocSizeOf aMallocSizeOf) {
1363   StaticMutexAutoLock locker(gTelemetryEventsMutex);
1364   size_t n = 0;
1365 
1366   auto getSizeOfRecords = [aMallocSizeOf](auto& storageMap) {
1367     size_t partial = storageMap.ShallowSizeOfExcludingThis(aMallocSizeOf);
1368     for (auto iter = storageMap.Iter(); !iter.Done(); iter.Next()) {
1369       EventRecordArray* eventRecords = iter.UserData();
1370       partial += eventRecords->ShallowSizeOfIncludingThis(aMallocSizeOf);
1371 
1372       const uint32_t len = eventRecords->Length();
1373       for (uint32_t i = 0; i < len; ++i) {
1374         partial += (*eventRecords)[i].SizeOfExcludingThis(aMallocSizeOf);
1375       }
1376     }
1377     return partial;
1378   };
1379 
1380   n += getSizeOfRecords(gEventRecords);
1381 
1382   n += gEventNameIDMap.ShallowSizeOfExcludingThis(aMallocSizeOf);
1383   for (auto iter = gEventNameIDMap.ConstIter(); !iter.Done(); iter.Next()) {
1384     n += iter.Key().SizeOfExcludingThisIfUnshared(aMallocSizeOf);
1385   }
1386 
1387   n += gCategoryNames.ShallowSizeOfExcludingThis(aMallocSizeOf);
1388   n += gEnabledCategories.ShallowSizeOfExcludingThis(aMallocSizeOf);
1389 
1390   if (gDynamicEventInfo) {
1391     n += gDynamicEventInfo->ShallowSizeOfIncludingThis(aMallocSizeOf);
1392     for (auto& info : *gDynamicEventInfo) {
1393       n += info.SizeOfExcludingThis(aMallocSizeOf);
1394     }
1395   }
1396 
1397   return n;
1398 }
1399