Source code

Revision control

Other Tools

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