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