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