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