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