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(×tamp)))) {
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(×tamp)))) {
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