Line data Source code
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 <prtime.h>
8 : #include "nsITelemetry.h"
9 : #include "nsHashKeys.h"
10 : #include "nsDataHashtable.h"
11 : #include "nsClassHashtable.h"
12 : #include "nsTArray.h"
13 : #include "mozilla/StaticMutex.h"
14 : #include "mozilla/Unused.h"
15 : #include "mozilla/Maybe.h"
16 : #include "mozilla/StaticPtr.h"
17 : #include "mozilla/Pair.h"
18 : #include "jsapi.h"
19 : #include "nsJSUtils.h"
20 : #include "nsXULAppAPI.h"
21 : #include "nsUTF8Utils.h"
22 :
23 : #include "TelemetryCommon.h"
24 : #include "TelemetryEvent.h"
25 : #include "TelemetryEventData.h"
26 : #include "ipc/TelemetryIPCAccumulator.h"
27 :
28 : using mozilla::StaticMutex;
29 : using mozilla::StaticMutexAutoLock;
30 : using mozilla::ArrayLength;
31 : using mozilla::Maybe;
32 : using mozilla::Nothing;
33 : using mozilla::StaticAutoPtr;
34 : using mozilla::TimeStamp;
35 : using mozilla::Telemetry::Common::AutoHashtable;
36 : using mozilla::Telemetry::Common::IsExpiredVersion;
37 : using mozilla::Telemetry::Common::CanRecordDataset;
38 : using mozilla::Telemetry::Common::IsInDataset;
39 : using mozilla::Telemetry::Common::MsSinceProcessStart;
40 : using mozilla::Telemetry::Common::LogToBrowserConsole;
41 : using mozilla::Telemetry::Common::CanRecordInProcess;
42 : using mozilla::Telemetry::Common::GetNameForProcessID;
43 : using mozilla::Telemetry::EventExtraEntry;
44 : using mozilla::Telemetry::ChildEventData;
45 : using mozilla::Telemetry::ProcessID;
46 :
47 : namespace TelemetryIPCAccumulator = mozilla::TelemetryIPCAccumulator;
48 :
49 : ////////////////////////////////////////////////////////////////////////
50 : ////////////////////////////////////////////////////////////////////////
51 : //
52 : // Naming: there are two kinds of functions in this file:
53 : //
54 : // * Functions taking a StaticMutexAutoLock: these can only be reached via
55 : // an interface function (TelemetryEvent::*). They expect the interface
56 : // function to have acquired |gTelemetryEventsMutex|, so they do not
57 : // have to be thread-safe.
58 : //
59 : // * Functions named TelemetryEvent::*. This is the external interface.
60 : // Entries and exits to these functions are serialised using
61 : // |gTelemetryEventsMutex|.
62 : //
63 : // Avoiding races and deadlocks:
64 : //
65 : // All functions in the external interface (TelemetryEvent::*) are
66 : // serialised using the mutex |gTelemetryEventsMutex|. This means
67 : // that the external interface is thread-safe, and the internal
68 : // functions can ignore thread safety. But it also brings a danger
69 : // of deadlock if any function in the external interface can get back
70 : // to that interface. That is, we will deadlock on any call chain like
71 : // this:
72 : //
73 : // TelemetryEvent::* -> .. any functions .. -> TelemetryEvent::*
74 : //
75 : // To reduce the danger of that happening, observe the following rules:
76 : //
77 : // * No function in TelemetryEvent::* may directly call, nor take the
78 : // address of, any other function in TelemetryEvent::*.
79 : //
80 : // * No internal function may call, nor take the address
81 : // of, any function in TelemetryEvent::*.
82 :
83 : ////////////////////////////////////////////////////////////////////////
84 : ////////////////////////////////////////////////////////////////////////
85 : //
86 : // PRIVATE TYPES
87 :
88 : namespace {
89 :
90 : const uint32_t kEventCount = mozilla::Telemetry::EventID::EventCount;
91 : // This is a special event id used to mark expired events, to make expiry checks
92 : // faster at runtime.
93 : const uint32_t kExpiredEventId = kEventCount + 1;
94 : static_assert(kEventCount < kExpiredEventId, "Should not overflow.");
95 :
96 : // This is the hard upper limit on the number of event records we keep in storage.
97 : // If we cross this limit, we will drop any further event recording until elements
98 : // are removed from storage.
99 : const uint32_t kMaxEventRecords = 1000;
100 : // Maximum length of any passed value string, in UTF8 byte sequence length.
101 : const uint32_t kMaxValueByteLength = 80;
102 : // Maximum length of any string value in the extra dictionary, in UTF8 byte sequence length.
103 : const uint32_t kMaxExtraValueByteLength = 80;
104 :
105 : typedef nsDataHashtable<nsCStringHashKey, uint32_t> StringUintMap;
106 : typedef nsClassHashtable<nsCStringHashKey, nsCString> StringMap;
107 :
108 : enum class RecordEventResult {
109 : Ok,
110 : UnknownEvent,
111 : InvalidExtraKey,
112 : StorageLimitReached,
113 : ExpiredEvent,
114 : WrongProcess,
115 : };
116 :
117 : typedef nsTArray<EventExtraEntry> ExtraArray;
118 :
119 0 : class EventRecord {
120 : public:
121 0 : EventRecord(double timestamp, uint32_t eventId, const Maybe<nsCString>& value,
122 : const ExtraArray& extra)
123 0 : : mTimestamp(timestamp)
124 : , mEventId(eventId)
125 : , mValue(value)
126 0 : , mExtra(extra)
127 0 : {}
128 :
129 0 : EventRecord(const EventRecord& other)
130 0 : : mTimestamp(other.mTimestamp)
131 0 : , mEventId(other.mEventId)
132 : , mValue(other.mValue)
133 0 : , mExtra(other.mExtra)
134 0 : {}
135 :
136 : EventRecord& operator=(const EventRecord& other) = delete;
137 :
138 0 : double Timestamp() const { return mTimestamp; }
139 0 : uint32_t EventId() const { return mEventId; }
140 0 : const Maybe<nsCString>& Value() const { return mValue; }
141 0 : const ExtraArray& Extra() const { return mExtra; }
142 :
143 : size_t SizeOfExcludingThis(mozilla::MallocSizeOf aMallocSizeOf) const;
144 :
145 : private:
146 : const double mTimestamp;
147 : const uint32_t mEventId;
148 : const Maybe<nsCString> mValue;
149 : const ExtraArray mExtra;
150 : };
151 :
152 : // Implements the methods for EventInfo.
153 : const char*
154 78 : EventInfo::method() const
155 : {
156 78 : return &gEventsStringTable[this->method_offset];
157 : }
158 :
159 : const char*
160 78 : EventInfo::object() const
161 : {
162 78 : return &gEventsStringTable[this->object_offset];
163 : }
164 :
165 : // Implements the methods for CommonEventInfo.
166 : const char*
167 165 : CommonEventInfo::category() const
168 : {
169 165 : return &gEventsStringTable[this->category_offset];
170 : }
171 :
172 : const char*
173 78 : CommonEventInfo::expiration_version() const
174 : {
175 78 : return &gEventsStringTable[this->expiration_version_offset];
176 : }
177 :
178 : const char*
179 0 : CommonEventInfo::extra_key(uint32_t index) const
180 : {
181 0 : MOZ_ASSERT(index < this->extra_count);
182 0 : uint32_t key_index = gExtraKeysTable[this->extra_index + index];
183 0 : return &gEventsStringTable[key_index];
184 : }
185 :
186 : // Implementation for the EventRecord class.
187 : size_t
188 0 : EventRecord::SizeOfExcludingThis(mozilla::MallocSizeOf aMallocSizeOf) const
189 : {
190 0 : size_t n = 0;
191 :
192 0 : if (mValue) {
193 0 : n += mValue.value().SizeOfExcludingThisIfUnshared(aMallocSizeOf);
194 : }
195 :
196 0 : n += mExtra.ShallowSizeOfExcludingThis(aMallocSizeOf);
197 0 : for (uint32_t i = 0; i < mExtra.Length(); ++i) {
198 0 : n += mExtra[i].key.SizeOfExcludingThisIfUnshared(aMallocSizeOf);
199 0 : n += mExtra[i].value.SizeOfExcludingThisIfUnshared(aMallocSizeOf);
200 : }
201 :
202 0 : return n;
203 : }
204 :
205 : nsCString
206 78 : UniqueEventName(const nsACString& category, const nsACString& method, const nsACString& object)
207 : {
208 78 : nsCString name;
209 78 : name.Append(category);
210 78 : name.AppendLiteral("#");
211 78 : name.Append(method);
212 78 : name.AppendLiteral("#");
213 78 : name.Append(object);
214 78 : return name;
215 : }
216 :
217 : nsCString
218 78 : UniqueEventName(const EventInfo& info)
219 : {
220 156 : return UniqueEventName(nsDependentCString(info.common_info.category()),
221 156 : nsDependentCString(info.method()),
222 234 : nsDependentCString(info.object()));
223 : }
224 :
225 : bool
226 72 : IsExpiredDate(uint32_t expires_days_since_epoch) {
227 72 : if (expires_days_since_epoch == 0) {
228 63 : return false;
229 : }
230 :
231 9 : const uint32_t days_since_epoch = PR_Now() / (PRTime(PR_USEC_PER_SEC) * 24 * 60 * 60);
232 9 : return expires_days_since_epoch <= days_since_epoch;
233 : }
234 :
235 : void
236 0 : TruncateToByteLength(nsCString& str, uint32_t length)
237 : {
238 : // last will be the index of the first byte of the current multi-byte sequence.
239 0 : uint32_t last = RewindToPriorUTF8Codepoint(str.get(), length);
240 0 : str.Truncate(last);
241 0 : }
242 :
243 : } // anonymous namespace
244 :
245 : ////////////////////////////////////////////////////////////////////////
246 : ////////////////////////////////////////////////////////////////////////
247 : //
248 : // PRIVATE STATE, SHARED BY ALL THREADS
249 :
250 : namespace {
251 :
252 : // Set to true once this global state has been initialized.
253 : bool gInitDone = false;
254 :
255 : bool gCanRecordBase;
256 : bool gCanRecordExtended;
257 :
258 : // The EventName -> EventID cache map.
259 3 : StringUintMap gEventNameIDMap(kEventCount);
260 :
261 : // The CategoryName -> CategoryID cache map.
262 3 : StringUintMap gCategoryNameIDMap;
263 :
264 : // This tracks the IDs of the categories for which recording is enabled.
265 3 : nsTHashtable<nsUint32HashKey> gEnabledCategories;
266 :
267 : // The main event storage. Events are inserted here, keyed by process id and
268 : // in recording order.
269 : typedef nsTArray<EventRecord> EventRecordArray;
270 3 : nsClassHashtable<nsUint32HashKey, EventRecordArray> gEventRecords;
271 :
272 : } // namespace
273 :
274 : ////////////////////////////////////////////////////////////////////////
275 : ////////////////////////////////////////////////////////////////////////
276 : //
277 : // PRIVATE: thread-safe helpers for event recording.
278 :
279 : namespace {
280 :
281 : bool
282 0 : CanRecordEvent(const StaticMutexAutoLock& lock, const CommonEventInfo& info,
283 : ProcessID process)
284 : {
285 0 : if (!gCanRecordBase) {
286 0 : return false;
287 : }
288 :
289 0 : if (!CanRecordDataset(info.dataset, gCanRecordBase, gCanRecordExtended)) {
290 0 : return false;
291 : }
292 :
293 0 : if (!CanRecordInProcess(info.record_in_processes, process)) {
294 0 : return false;
295 : }
296 :
297 0 : return gEnabledCategories.GetEntry(info.category_offset);
298 : }
299 :
300 : EventRecordArray*
301 0 : GetEventRecordsForProcess(const StaticMutexAutoLock& lock, ProcessID processType)
302 : {
303 0 : EventRecordArray* eventRecords = nullptr;
304 0 : if (!gEventRecords.Get(uint32_t(processType), &eventRecords)) {
305 0 : eventRecords = new EventRecordArray();
306 0 : gEventRecords.Put(uint32_t(processType), eventRecords);
307 : }
308 0 : return eventRecords;
309 : }
310 :
311 : bool
312 0 : GetEventId(const StaticMutexAutoLock& lock, const nsACString& category,
313 : const nsACString& method, const nsACString& object,
314 : uint32_t* eventId)
315 : {
316 0 : MOZ_ASSERT(eventId);
317 0 : const nsCString& name = UniqueEventName(category, method, object);
318 0 : return gEventNameIDMap.Get(name, eventId);
319 : }
320 :
321 : RecordEventResult
322 0 : RecordEvent(const StaticMutexAutoLock& lock, ProcessID processType,
323 : double timestamp, const nsACString& category,
324 : const nsACString& method, const nsACString& object,
325 : const Maybe<nsCString>& value, const ExtraArray& extra)
326 : {
327 0 : EventRecordArray* eventRecords = GetEventRecordsForProcess(lock, processType);
328 :
329 : // Apply hard limit on event count in storage.
330 0 : if (eventRecords->Length() >= kMaxEventRecords) {
331 0 : return RecordEventResult::StorageLimitReached;
332 : }
333 :
334 : // Look up the event id.
335 : uint32_t eventId;
336 0 : if (!GetEventId(lock, category, method, object, &eventId)) {
337 0 : return RecordEventResult::UnknownEvent;
338 : }
339 :
340 : // If the event is expired or not enabled for this process, we silently drop this call.
341 : // We don't want recording for expired probes to be an error so code doesn't
342 : // have to be removed at a specific time or version.
343 : // Even logging warnings would become very noisy.
344 0 : if (eventId == kExpiredEventId) {
345 0 : return RecordEventResult::ExpiredEvent;
346 : }
347 :
348 : // Check whether we can record this event.
349 0 : const CommonEventInfo& common = gEventInfo[eventId].common_info;
350 0 : if (!CanRecordEvent(lock, common, processType)) {
351 0 : return RecordEventResult::Ok;
352 : }
353 :
354 : // Check whether the extra keys passed are valid.
355 0 : nsTHashtable<nsCStringHashKey> validExtraKeys;
356 0 : for (uint32_t i = 0; i < common.extra_count; ++i) {
357 0 : validExtraKeys.PutEntry(nsDependentCString(common.extra_key(i)));
358 : }
359 :
360 0 : for (uint32_t i = 0; i < extra.Length(); ++i) {
361 0 : if (!validExtraKeys.GetEntry(extra[i].key)) {
362 0 : return RecordEventResult::InvalidExtraKey;
363 : }
364 : }
365 :
366 : // Add event record.
367 0 : eventRecords->AppendElement(EventRecord(timestamp, eventId, value, extra));
368 0 : return RecordEventResult::Ok;
369 : }
370 :
371 : RecordEventResult
372 0 : ShouldRecordChildEvent(const StaticMutexAutoLock& lock, const nsACString& category,
373 : const nsACString& method, const nsACString& object)
374 : {
375 : uint32_t eventId;
376 0 : if (!GetEventId(lock, category, method, object, &eventId)) {
377 0 : return RecordEventResult::UnknownEvent;
378 : }
379 :
380 0 : if (eventId == kExpiredEventId) {
381 0 : return RecordEventResult::ExpiredEvent;
382 : }
383 :
384 0 : const auto processes = gEventInfo[eventId].common_info.record_in_processes;
385 0 : if (!CanRecordInProcess(processes, XRE_GetProcessType())) {
386 0 : return RecordEventResult::WrongProcess;
387 : }
388 :
389 0 : return RecordEventResult::Ok;
390 : }
391 :
392 : } // anonymous namespace
393 :
394 : ////////////////////////////////////////////////////////////////////////
395 : ////////////////////////////////////////////////////////////////////////
396 : //
397 : // PRIVATE: thread-unsafe helpers for event handling.
398 :
399 : namespace {
400 :
401 : nsresult
402 0 : SerializeEventsArray(const EventRecordArray& events,
403 : JSContext* cx,
404 : JS::MutableHandleObject result)
405 : {
406 : // We serialize the events to a JS array.
407 0 : JS::RootedObject eventsArray(cx, JS_NewArrayObject(cx, events.Length()));
408 0 : if (!eventsArray) {
409 0 : return NS_ERROR_FAILURE;
410 : }
411 :
412 0 : for (uint32_t i = 0; i < events.Length(); ++i) {
413 0 : const EventRecord& record = events[i];
414 0 : const EventInfo& info = gEventInfo[record.EventId()];
415 :
416 : // Each entry is an array of one of the forms:
417 : // [timestamp, category, method, object, value]
418 : // [timestamp, category, method, object, null, extra]
419 : // [timestamp, category, method, object, value, extra]
420 0 : JS::AutoValueVector items(cx);
421 :
422 : // Add timestamp.
423 0 : JS::Rooted<JS::Value> val(cx);
424 0 : if (!items.append(JS::NumberValue(floor(record.Timestamp())))) {
425 0 : return NS_ERROR_FAILURE;
426 : }
427 :
428 : // Add category, method, object.
429 : const char* strings[] = {
430 0 : info.common_info.category(),
431 0 : info.method(),
432 0 : info.object(),
433 0 : };
434 0 : for (const char* s : strings) {
435 0 : const NS_ConvertUTF8toUTF16 wide(s);
436 0 : if (!items.append(JS::StringValue(JS_NewUCStringCopyN(cx, wide.Data(), wide.Length())))) {
437 0 : return NS_ERROR_FAILURE;
438 : }
439 : }
440 :
441 : // Add the optional string value only when needed.
442 : // When the value field is empty and extra is not set, we can save a little space that way.
443 : // We still need to submit a null value if extra is set, to match the form:
444 : // [ts, category, method, object, null, extra]
445 0 : if (record.Value()) {
446 0 : const NS_ConvertUTF8toUTF16 wide(record.Value().value());
447 0 : if (!items.append(JS::StringValue(JS_NewUCStringCopyN(cx, wide.Data(), wide.Length())))) {
448 0 : return NS_ERROR_FAILURE;
449 : }
450 0 : } else if (!record.Extra().IsEmpty()) {
451 0 : if (!items.append(JS::NullValue())) {
452 0 : return NS_ERROR_FAILURE;
453 : }
454 : }
455 :
456 : // Add the optional extra dictionary.
457 : // To save a little space, only add it when it is not empty.
458 0 : if (!record.Extra().IsEmpty()) {
459 0 : JS::RootedObject obj(cx, JS_NewPlainObject(cx));
460 0 : if (!obj) {
461 0 : return NS_ERROR_FAILURE;
462 : }
463 :
464 : // Add extra key & value entries.
465 0 : const ExtraArray& extra = record.Extra();
466 0 : for (uint32_t i = 0; i < extra.Length(); ++i) {
467 0 : const NS_ConvertUTF8toUTF16 wide(extra[i].value);
468 0 : JS::Rooted<JS::Value> value(cx);
469 0 : value.setString(JS_NewUCStringCopyN(cx, wide.Data(), wide.Length()));
470 :
471 0 : if (!JS_DefineProperty(cx, obj, extra[i].key.get(), value, JSPROP_ENUMERATE)) {
472 0 : return NS_ERROR_FAILURE;
473 : }
474 : }
475 0 : val.setObject(*obj);
476 :
477 0 : if (!items.append(val)) {
478 0 : return NS_ERROR_FAILURE;
479 : }
480 : }
481 :
482 : // Add the record to the events array.
483 0 : JS::RootedObject itemsArray(cx, JS_NewArrayObject(cx, items));
484 0 : if (!JS_DefineElement(cx, eventsArray, i, itemsArray, JSPROP_ENUMERATE)) {
485 0 : return NS_ERROR_FAILURE;
486 : }
487 : }
488 :
489 0 : result.set(eventsArray);
490 0 : return NS_OK;
491 : }
492 :
493 : } // anonymous namespace
494 :
495 : ////////////////////////////////////////////////////////////////////////
496 : ////////////////////////////////////////////////////////////////////////
497 : //
498 : // EXTERNALLY VISIBLE FUNCTIONS in namespace TelemetryEvents::
499 :
500 : // This is a StaticMutex rather than a plain Mutex (1) so that
501 : // it gets initialised in a thread-safe manner the first time
502 : // it is used, and (2) because it is never de-initialised, and
503 : // a normal Mutex would show up as a leak in BloatView. StaticMutex
504 : // also has the "OffTheBooks" property, so it won't show as a leak
505 : // in BloatView.
506 : // Another reason to use a StaticMutex instead of a plain Mutex is
507 : // that, due to the nature of Telemetry, we cannot rely on having a
508 : // mutex initialized in InitializeGlobalState. Unfortunately, we
509 : // cannot make sure that no other function is called before this point.
510 3 : static StaticMutex gTelemetryEventsMutex;
511 :
512 : void
513 3 : TelemetryEvent::InitializeGlobalState(bool aCanRecordBase, bool aCanRecordExtended)
514 : {
515 6 : StaticMutexAutoLock locker(gTelemetryEventsMutex);
516 3 : MOZ_ASSERT(!gInitDone, "TelemetryEvent::InitializeGlobalState "
517 : "may only be called once");
518 :
519 3 : gCanRecordBase = aCanRecordBase;
520 3 : gCanRecordExtended = aCanRecordExtended;
521 :
522 : // Populate the static event name->id cache. Note that the event names are
523 : // statically allocated and come from the automatically generated TelemetryEventData.h.
524 3 : const uint32_t eventCount = static_cast<uint32_t>(mozilla::Telemetry::EventID::EventCount);
525 81 : for (uint32_t i = 0; i < eventCount; ++i) {
526 78 : const EventInfo& info = gEventInfo[i];
527 78 : uint32_t eventId = i;
528 :
529 : // If this event is expired or not recorded in this process, mark it with
530 : // a special event id.
531 : // This avoids doing repeated checks at runtime.
532 150 : if (IsExpiredVersion(info.common_info.expiration_version()) ||
533 72 : IsExpiredDate(info.common_info.expiration_day)) {
534 12 : eventId = kExpiredEventId;
535 : }
536 :
537 78 : gEventNameIDMap.Put(UniqueEventName(info), eventId);
538 78 : if (!gCategoryNameIDMap.Contains(nsDependentCString(info.common_info.category()))) {
539 18 : gCategoryNameIDMap.Put(nsDependentCString(info.common_info.category()),
540 18 : info.common_info.category_offset);
541 : }
542 : }
543 :
544 : #ifdef DEBUG
545 3 : gEventNameIDMap.MarkImmutable();
546 3 : gCategoryNameIDMap.MarkImmutable();
547 : #endif
548 3 : gInitDone = true;
549 3 : }
550 :
551 : void
552 0 : TelemetryEvent::DeInitializeGlobalState()
553 : {
554 0 : StaticMutexAutoLock locker(gTelemetryEventsMutex);
555 0 : MOZ_ASSERT(gInitDone);
556 :
557 0 : gCanRecordBase = false;
558 0 : gCanRecordExtended = false;
559 :
560 0 : gEventNameIDMap.Clear();
561 0 : gCategoryNameIDMap.Clear();
562 0 : gEnabledCategories.Clear();
563 0 : gEventRecords.Clear();
564 :
565 0 : gInitDone = false;
566 0 : }
567 :
568 : void
569 0 : TelemetryEvent::SetCanRecordBase(bool b)
570 : {
571 0 : StaticMutexAutoLock locker(gTelemetryEventsMutex);
572 0 : gCanRecordBase = b;
573 0 : }
574 :
575 : void
576 3 : TelemetryEvent::SetCanRecordExtended(bool b) {
577 6 : StaticMutexAutoLock locker(gTelemetryEventsMutex);
578 3 : gCanRecordExtended = b;
579 3 : }
580 :
581 : nsresult
582 0 : TelemetryEvent::RecordChildEvents(ProcessID aProcessType,
583 : const nsTArray<mozilla::Telemetry::ChildEventData>& aEvents)
584 : {
585 0 : MOZ_ASSERT(XRE_IsParentProcess());
586 0 : StaticMutexAutoLock locker(gTelemetryEventsMutex);
587 0 : for (uint32_t i = 0; i < aEvents.Length(); ++i) {
588 0 : const mozilla::Telemetry::ChildEventData e = aEvents[i];
589 :
590 : // Timestamps from child processes are absolute. We fix them up here to be
591 : // relative to the main process start time.
592 : // This allows us to put events from all processes on the same timeline.
593 0 : double relativeTimestamp = (e.timestamp - TimeStamp::ProcessCreation()).ToMilliseconds();
594 :
595 0 : ::RecordEvent(locker, aProcessType, relativeTimestamp, e.category, e.method, e.object, e.value, e.extra);
596 : }
597 0 : return NS_OK;
598 : }
599 :
600 : nsresult
601 0 : TelemetryEvent::RecordEvent(const nsACString& aCategory, const nsACString& aMethod,
602 : const nsACString& aObject, JS::HandleValue aValue,
603 : JS::HandleValue aExtra, JSContext* cx,
604 : uint8_t optional_argc)
605 : {
606 : // Check value argument.
607 0 : if ((optional_argc > 0) && !aValue.isNull() && !aValue.isString()) {
608 0 : LogToBrowserConsole(nsIScriptError::warningFlag,
609 0 : NS_LITERAL_STRING("Invalid type for value parameter."));
610 0 : return NS_OK;
611 : }
612 :
613 : // Extract value parameter.
614 0 : Maybe<nsCString> value;
615 0 : if (aValue.isString()) {
616 0 : nsAutoJSString jsStr;
617 0 : if (!jsStr.init(cx, aValue)) {
618 0 : LogToBrowserConsole(nsIScriptError::warningFlag,
619 0 : NS_LITERAL_STRING("Invalid string value for value parameter."));
620 0 : return NS_OK;
621 : }
622 :
623 0 : nsCString str = NS_ConvertUTF16toUTF8(jsStr);
624 0 : if (str.Length() > kMaxValueByteLength) {
625 0 : LogToBrowserConsole(nsIScriptError::warningFlag,
626 0 : NS_LITERAL_STRING("Value parameter exceeds maximum string length, truncating."));
627 0 : TruncateToByteLength(str, kMaxValueByteLength);
628 : }
629 0 : value = mozilla::Some(str);
630 : }
631 :
632 : // Check extra argument.
633 0 : if ((optional_argc > 1) && !aExtra.isNull() && !aExtra.isObject()) {
634 0 : LogToBrowserConsole(nsIScriptError::warningFlag,
635 0 : NS_LITERAL_STRING("Invalid type for extra parameter."));
636 0 : return NS_OK;
637 : }
638 :
639 : // Extract extra dictionary.
640 0 : ExtraArray extra;
641 0 : if (aExtra.isObject()) {
642 0 : JS::RootedObject obj(cx, &aExtra.toObject());
643 0 : JS::Rooted<JS::IdVector> ids(cx, JS::IdVector(cx));
644 0 : if (!JS_Enumerate(cx, obj, &ids)) {
645 0 : LogToBrowserConsole(nsIScriptError::warningFlag,
646 0 : NS_LITERAL_STRING("Failed to enumerate object."));
647 0 : return NS_OK;
648 : }
649 :
650 0 : for (size_t i = 0, n = ids.length(); i < n; i++) {
651 0 : nsAutoJSString key;
652 0 : if (!key.init(cx, ids[i])) {
653 0 : LogToBrowserConsole(nsIScriptError::warningFlag,
654 0 : NS_LITERAL_STRING("Extra dictionary should only contain string keys."));
655 0 : return NS_OK;
656 : }
657 :
658 0 : JS::Rooted<JS::Value> value(cx);
659 0 : if (!JS_GetPropertyById(cx, obj, ids[i], &value)) {
660 0 : LogToBrowserConsole(nsIScriptError::warningFlag,
661 0 : NS_LITERAL_STRING("Failed to get extra property."));
662 0 : return NS_OK;
663 : }
664 :
665 0 : nsAutoJSString jsStr;
666 0 : if (!value.isString() || !jsStr.init(cx, value)) {
667 0 : LogToBrowserConsole(nsIScriptError::warningFlag,
668 0 : NS_LITERAL_STRING("Extra properties should have string values."));
669 0 : return NS_OK;
670 : }
671 :
672 0 : nsCString str = NS_ConvertUTF16toUTF8(jsStr);
673 0 : if (str.Length() > kMaxExtraValueByteLength) {
674 0 : LogToBrowserConsole(nsIScriptError::warningFlag,
675 0 : NS_LITERAL_STRING("Extra value exceeds maximum string length, truncating."));
676 0 : TruncateToByteLength(str, kMaxExtraValueByteLength);
677 : }
678 :
679 0 : extra.AppendElement(EventExtraEntry{NS_ConvertUTF16toUTF8(key), str});
680 : }
681 : }
682 :
683 : // Lock for accessing internal data.
684 : // While the lock is being held, no complex calls like JS calls can be made,
685 : // as all of these could record Telemetry, which would result in deadlock.
686 : RecordEventResult res;
687 0 : if (!XRE_IsParentProcess()) {
688 : {
689 0 : StaticMutexAutoLock lock(gTelemetryEventsMutex);
690 0 : res = ::ShouldRecordChildEvent(lock, aCategory, aMethod, aObject);
691 : }
692 :
693 0 : if (res == RecordEventResult::Ok) {
694 0 : TelemetryIPCAccumulator::RecordChildEvent(TimeStamp::NowLoRes(), aCategory,
695 0 : aMethod, aObject, value, extra);
696 : }
697 : } else {
698 0 : StaticMutexAutoLock lock(gTelemetryEventsMutex);
699 :
700 0 : if (!gInitDone) {
701 0 : return NS_ERROR_FAILURE;
702 : }
703 :
704 : // Get the current time.
705 0 : double timestamp = -1;
706 0 : if (NS_WARN_IF(NS_FAILED(MsSinceProcessStart(×tamp)))) {
707 0 : return NS_ERROR_FAILURE;
708 : }
709 :
710 0 : res = ::RecordEvent(lock, ProcessID::Parent, timestamp, aCategory, aMethod, aObject, value, extra);
711 : }
712 :
713 : // Trigger warnings or errors where needed.
714 0 : switch (res) {
715 : case RecordEventResult::UnknownEvent: {
716 0 : JS_ReportErrorASCII(cx, R"(Unknown event: ["%s", "%s", "%s"])",
717 0 : PromiseFlatCString(aCategory).get(),
718 0 : PromiseFlatCString(aMethod).get(),
719 0 : PromiseFlatCString(aObject).get());
720 0 : return NS_ERROR_INVALID_ARG;
721 : }
722 : case RecordEventResult::InvalidExtraKey:
723 0 : LogToBrowserConsole(nsIScriptError::warningFlag,
724 0 : NS_LITERAL_STRING("Invalid extra key for event."));
725 0 : return NS_OK;
726 : case RecordEventResult::StorageLimitReached:
727 0 : LogToBrowserConsole(nsIScriptError::warningFlag,
728 0 : NS_LITERAL_STRING("Event storage limit reached."));
729 0 : return NS_OK;
730 : default:
731 0 : return NS_OK;
732 : }
733 : }
734 :
735 : nsresult
736 0 : TelemetryEvent::CreateSnapshots(uint32_t aDataset, bool aClear, JSContext* cx,
737 : uint8_t optional_argc, JS::MutableHandleValue aResult)
738 : {
739 0 : if (!XRE_IsParentProcess()) {
740 0 : return NS_ERROR_FAILURE;
741 : }
742 :
743 : // Creating a JS snapshot of the events is a two-step process:
744 : // (1) Lock the storage and copy the events into function-local storage.
745 : // (2) Serialize the events into JS.
746 : // We can't hold a lock for (2) because we will run into deadlocks otherwise
747 : // from JS recording Telemetry.
748 :
749 : // (1) Extract the events from storage with a lock held.
750 0 : nsTArray<mozilla::Pair<const char*, EventRecordArray>> processEvents;
751 : {
752 0 : StaticMutexAutoLock locker(gTelemetryEventsMutex);
753 :
754 0 : if (!gInitDone) {
755 0 : return NS_ERROR_FAILURE;
756 : }
757 :
758 0 : for (auto iter = gEventRecords.Iter(); !iter.Done(); iter.Next()) {
759 0 : const EventRecordArray* eventStorage = static_cast<EventRecordArray*>(iter.Data());
760 0 : EventRecordArray events;
761 :
762 0 : const uint32_t len = eventStorage->Length();
763 0 : for (uint32_t i = 0; i < len; ++i) {
764 0 : const EventRecord& record = (*eventStorage)[i];
765 0 : const EventInfo& info = gEventInfo[record.EventId()];
766 :
767 0 : if (IsInDataset(info.common_info.dataset, aDataset)) {
768 0 : events.AppendElement(record);
769 : }
770 : }
771 :
772 0 : if (events.Length()) {
773 0 : const char* processName = GetNameForProcessID(ProcessID(iter.Key()));
774 0 : processEvents.AppendElement(mozilla::MakePair(processName, events));
775 : }
776 : }
777 :
778 0 : if (aClear) {
779 0 : gEventRecords.Clear();
780 : }
781 : }
782 :
783 : // (2) Serialize the events to a JS object.
784 0 : JS::RootedObject rootObj(cx, JS_NewPlainObject(cx));
785 0 : if (!rootObj) {
786 0 : return NS_ERROR_FAILURE;
787 : }
788 :
789 0 : const uint32_t processLength = processEvents.Length();
790 0 : for (uint32_t i = 0; i < processLength; ++i)
791 : {
792 0 : JS::RootedObject eventsArray(cx);
793 0 : if (NS_FAILED(SerializeEventsArray(processEvents[i].second(), cx, &eventsArray))) {
794 0 : return NS_ERROR_FAILURE;
795 : }
796 :
797 0 : if (!JS_DefineProperty(cx, rootObj, processEvents[i].first(), eventsArray, JSPROP_ENUMERATE)) {
798 0 : return NS_ERROR_FAILURE;
799 : }
800 : }
801 :
802 0 : aResult.setObject(*rootObj);
803 0 : return NS_OK;
804 : }
805 :
806 : /**
807 : * Resets all the stored events. This is intended to be only used in tests.
808 : */
809 : void
810 0 : TelemetryEvent::ClearEvents()
811 : {
812 0 : StaticMutexAutoLock lock(gTelemetryEventsMutex);
813 :
814 0 : if (!gInitDone) {
815 0 : return;
816 : }
817 :
818 0 : gEventRecords.Clear();
819 : }
820 :
821 : void
822 0 : TelemetryEvent::SetEventRecordingEnabled(const nsACString& category, bool enabled)
823 : {
824 0 : StaticMutexAutoLock locker(gTelemetryEventsMutex);
825 :
826 : uint32_t categoryId;
827 0 : if (!gCategoryNameIDMap.Get(category, &categoryId)) {
828 0 : LogToBrowserConsole(nsIScriptError::warningFlag,
829 0 : NS_LITERAL_STRING("Unkown category for SetEventRecordingEnabled."));
830 0 : return;
831 : }
832 :
833 0 : if (enabled) {
834 0 : gEnabledCategories.PutEntry(categoryId);
835 : } else {
836 0 : gEnabledCategories.RemoveEntry(categoryId);
837 : }
838 : }
839 :
840 : size_t
841 0 : TelemetryEvent::SizeOfIncludingThis(mozilla::MallocSizeOf aMallocSizeOf)
842 : {
843 0 : StaticMutexAutoLock locker(gTelemetryEventsMutex);
844 0 : size_t n = 0;
845 :
846 :
847 0 : n += gEventRecords.ShallowSizeOfExcludingThis(aMallocSizeOf);
848 0 : for (auto iter = gEventRecords.Iter(); !iter.Done(); iter.Next()) {
849 0 : EventRecordArray* eventRecords = static_cast<EventRecordArray*>(iter.Data());
850 0 : n += eventRecords->ShallowSizeOfIncludingThis(aMallocSizeOf);
851 :
852 0 : const uint32_t len = eventRecords->Length();
853 0 : for (uint32_t i = 0; i < len; ++i) {
854 0 : n += (*eventRecords)[i].SizeOfExcludingThis(aMallocSizeOf);
855 : }
856 : }
857 :
858 0 : n += gEventNameIDMap.ShallowSizeOfExcludingThis(aMallocSizeOf);
859 0 : for (auto iter = gEventNameIDMap.ConstIter(); !iter.Done(); iter.Next()) {
860 0 : n += iter.Key().SizeOfExcludingThisIfUnshared(aMallocSizeOf);
861 : }
862 :
863 0 : n += gCategoryNameIDMap.ShallowSizeOfExcludingThis(aMallocSizeOf);
864 0 : for (auto iter = gCategoryNameIDMap.ConstIter(); !iter.Done(); iter.Next()) {
865 0 : n += iter.Key().SizeOfExcludingThisIfUnshared(aMallocSizeOf);
866 : }
867 :
868 0 : n += gEnabledCategories.ShallowSizeOfExcludingThis(aMallocSizeOf);
869 :
870 0 : return n;
871 9 : }
|