summaryrefslogtreecommitdiffstats
path: root/toolkit/components/telemetry/core
diff options
context:
space:
mode:
Diffstat (limited to 'toolkit/components/telemetry/core')
-rw-r--r--toolkit/components/telemetry/core/EventInfo.h57
-rw-r--r--toolkit/components/telemetry/core/ScalarInfo.h94
-rw-r--r--toolkit/components/telemetry/core/Stopwatch.cpp752
-rw-r--r--toolkit/components/telemetry/core/Stopwatch.h84
-rw-r--r--toolkit/components/telemetry/core/Telemetry.cpp2035
-rw-r--r--toolkit/components/telemetry/core/Telemetry.h577
-rw-r--r--toolkit/components/telemetry/core/TelemetryCommon.cpp209
-rw-r--r--toolkit/components/telemetry/core/TelemetryCommon.h198
-rw-r--r--toolkit/components/telemetry/core/TelemetryEvent.cpp1387
-rw-r--r--toolkit/components/telemetry/core/TelemetryEvent.h71
-rw-r--r--toolkit/components/telemetry/core/TelemetryHistogram.cpp3677
-rw-r--r--toolkit/components/telemetry/core/TelemetryHistogram.h124
-rw-r--r--toolkit/components/telemetry/core/TelemetryScalar.cpp4190
-rw-r--r--toolkit/components/telemetry/core/TelemetryScalar.h133
-rw-r--r--toolkit/components/telemetry/core/TelemetryUserInteraction.cpp101
-rw-r--r--toolkit/components/telemetry/core/TelemetryUserInteraction.h20
-rw-r--r--toolkit/components/telemetry/core/UserInteractionInfo.h30
-rw-r--r--toolkit/components/telemetry/core/components.conf21
-rw-r--r--toolkit/components/telemetry/core/ipc/TelemetryComms.h400
-rw-r--r--toolkit/components/telemetry/core/ipc/TelemetryIPC.cpp59
-rw-r--r--toolkit/components/telemetry/core/ipc/TelemetryIPC.h114
-rw-r--r--toolkit/components/telemetry/core/ipc/TelemetryIPCAccumulator.cpp351
-rw-r--r--toolkit/components/telemetry/core/ipc/TelemetryIPCAccumulator.h56
-rw-r--r--toolkit/components/telemetry/core/nsITelemetry.idl680
24 files changed, 15420 insertions, 0 deletions
diff --git a/toolkit/components/telemetry/core/EventInfo.h b/toolkit/components/telemetry/core/EventInfo.h
new file mode 100644
index 0000000000..b80f85af92
--- /dev/null
+++ b/toolkit/components/telemetry/core/EventInfo.h
@@ -0,0 +1,57 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2; -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef TelemetryEventInfo_h__
+#define TelemetryEventInfo_h__
+
+#include "TelemetryCommon.h"
+
+// This module is internal to Telemetry. The structures here hold data that
+// describe events.
+// It should only be used by TelemetryEventData.h and TelemetryEvent.cpp.
+//
+// For the public interface to Telemetry functionality, see Telemetry.h.
+
+namespace {
+
+struct CommonEventInfo {
+ // Indices for the category and expiration strings.
+ uint32_t category_offset;
+ uint32_t expiration_version_offset;
+
+ // The index and count for the extra key offsets in the extra table.
+ uint32_t extra_index;
+ uint32_t extra_count;
+
+ // The dataset this event is recorded in.
+ uint32_t dataset;
+
+ // Which processes to record this event in.
+ mozilla::Telemetry::Common::RecordedProcessType record_in_processes;
+
+ // Which products to record this event on.
+ mozilla::Telemetry::Common::SupportedProduct products;
+
+ // Convenience functions for accessing event strings.
+ const nsDependentCString expiration_version() const;
+ const nsDependentCString category() const;
+ const nsDependentCString extra_key(uint32_t index) const;
+};
+
+struct EventInfo {
+ // The corresponding CommonEventInfo.
+ const CommonEventInfo& common_info;
+
+ // Indices for the method & object strings.
+ uint32_t method_offset;
+ uint32_t object_offset;
+
+ const nsDependentCString method() const;
+ const nsDependentCString object() const;
+};
+
+} // namespace
+
+#endif // TelemetryEventInfo_h__
diff --git a/toolkit/components/telemetry/core/ScalarInfo.h b/toolkit/components/telemetry/core/ScalarInfo.h
new file mode 100644
index 0000000000..125ea7e88e
--- /dev/null
+++ b/toolkit/components/telemetry/core/ScalarInfo.h
@@ -0,0 +1,94 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2; -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef TelemetryScalarInfo_h__
+#define TelemetryScalarInfo_h__
+
+#include "TelemetryCommon.h"
+
+// This module is internal to Telemetry. It defines a structure that holds the
+// scalar info. It should only be used by TelemetryScalarData.h automatically
+// generated file and TelemetryScalar.cpp. This should not be used anywhere
+// else. For the public interface to Telemetry functionality, see Telemetry.h.
+
+namespace {
+
+/**
+ * Base scalar information, common to both "static" and dynamic scalars.
+ */
+struct BaseScalarInfo {
+ uint32_t kind;
+ uint32_t dataset;
+ mozilla::Telemetry::Common::RecordedProcessType record_in_processes;
+ bool keyed;
+ uint32_t key_count;
+ uint32_t key_offset;
+ mozilla::Telemetry::Common::SupportedProduct products;
+ bool builtin;
+
+ constexpr BaseScalarInfo(
+ uint32_t aKind, uint32_t aDataset,
+ mozilla::Telemetry::Common::RecordedProcessType aRecordInProcess,
+ bool aKeyed, uint32_t aKeyCount, uint32_t aKeyOffset,
+ mozilla::Telemetry::Common::SupportedProduct aProducts,
+ bool aBuiltin = true)
+ : kind(aKind),
+ dataset(aDataset),
+ record_in_processes(aRecordInProcess),
+ keyed(aKeyed),
+ key_count(aKeyCount),
+ key_offset(aKeyOffset),
+ products(aProducts),
+ builtin(aBuiltin) {}
+ virtual ~BaseScalarInfo() = default;
+
+ virtual const char* name() const = 0;
+ virtual const char* expiration() const = 0;
+
+ virtual uint32_t storeOffset() const = 0;
+ virtual uint32_t storeCount() const = 0;
+};
+
+/**
+ * "Static" scalar definition: these are the ones riding
+ * the trains.
+ */
+struct ScalarInfo : BaseScalarInfo {
+ uint32_t name_offset;
+ uint32_t expiration_offset;
+ uint32_t store_count;
+ uint16_t store_offset;
+
+ // In order to cleanly support dynamic scalars in TelemetryScalar.cpp, we need
+ // to use virtual functions for |name| and |expiration|, as they won't be
+ // looked up in the static tables in that case. However, using virtual
+ // functions makes |ScalarInfo| non-aggregate and prevents from using
+ // aggregate initialization (curly brackets) in the generated
+ // TelemetryScalarData.h. To work around this problem we define a constructor
+ // that takes the exact number of parameters we need.
+ constexpr ScalarInfo(
+ uint32_t aKind, uint32_t aNameOffset, uint32_t aExpirationOffset,
+ uint32_t aDataset,
+ mozilla::Telemetry::Common::RecordedProcessType aRecordInProcess,
+ bool aKeyed, uint32_t aKeyCount, uint32_t aKeyOffset,
+ mozilla::Telemetry::Common::SupportedProduct aProducts,
+ uint32_t aStoreCount, uint32_t aStoreOffset)
+ : BaseScalarInfo(aKind, aDataset, aRecordInProcess, aKeyed, aKeyCount,
+ aKeyOffset, aProducts),
+ name_offset(aNameOffset),
+ expiration_offset(aExpirationOffset),
+ store_count(aStoreCount),
+ store_offset(aStoreOffset) {}
+
+ const char* name() const override;
+ const char* expiration() const override;
+
+ uint32_t storeOffset() const override { return store_offset; };
+ uint32_t storeCount() const override { return store_count; };
+};
+
+} // namespace
+
+#endif // TelemetryScalarInfo_h__
diff --git a/toolkit/components/telemetry/core/Stopwatch.cpp b/toolkit/components/telemetry/core/Stopwatch.cpp
new file mode 100644
index 0000000000..fe6191aed3
--- /dev/null
+++ b/toolkit/components/telemetry/core/Stopwatch.cpp
@@ -0,0 +1,752 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "mozilla/telemetry/Stopwatch.h"
+
+#include "TelemetryHistogram.h"
+#include "TelemetryUserInteraction.h"
+
+#include "js/MapAndSet.h"
+#include "js/WeakMap.h"
+#include "mozilla/dom/ScriptSettings.h"
+#include "mozilla/BackgroundHangMonitor.h"
+#include "mozilla/ClearOnShutdown.h"
+#include "mozilla/HangAnnotations.h"
+#include "mozilla/ProfilerMarkers.h"
+#include "mozilla/DataMutex.h"
+#include "mozilla/TimeStamp.h"
+#include "nsHashKeys.h"
+#include "nsContentUtils.h"
+#include "nsPrintfCString.h"
+#include "nsQueryObject.h"
+#include "nsString.h"
+#include "xpcpublic.h"
+
+using mozilla::DataMutex;
+using mozilla::dom::AutoJSAPI;
+
+#define USER_INTERACTION_VALUE_MAX_LENGTH 50 // bytes
+
+static inline nsQueryObject<nsISupports> do_QueryReflector(
+ JSObject* aReflector) {
+ // None of the types we query to are implemented by Window or Location.
+ nsCOMPtr<nsISupports> reflector = xpc::ReflectorToISupportsStatic(aReflector);
+ return do_QueryObject(reflector);
+}
+
+static inline nsQueryObject<nsISupports> do_QueryReflector(
+ const JS::Value& aReflector) {
+ return do_QueryReflector(&aReflector.toObject());
+}
+
+static void LogError(JSContext* aCx, const nsCString& aMessage) {
+ // This is a bit of a hack to report an error with the current JS caller's
+ // location. We create an AutoJSAPI object bound to the current caller
+ // global, report a JS error, and then let AutoJSAPI's destructor report the
+ // error.
+ //
+ // Unfortunately, there isn't currently a more straightforward way to do
+ // this from C++.
+ JS::Rooted<JSObject*> global(aCx, JS::CurrentGlobalOrNull(aCx));
+
+ AutoJSAPI jsapi;
+ if (jsapi.Init(global)) {
+ JS_ReportErrorUTF8(jsapi.cx(), "%s", aMessage.get());
+ }
+}
+
+namespace mozilla::telemetry {
+
+class Timer final : public mozilla::LinkedListElement<RefPtr<Timer>> {
+ public:
+ NS_INLINE_DECL_REFCOUNTING(Timer)
+
+ Timer() = default;
+
+ void Start(bool aInSeconds) {
+ mStartTime = TimeStamp::Now();
+ mInSeconds = aInSeconds;
+ }
+
+ bool Started() { return !mStartTime.IsNull(); }
+
+ uint32_t Elapsed() {
+ auto delta = TimeStamp::Now() - mStartTime;
+ return mInSeconds ? delta.ToSeconds() : delta.ToMilliseconds();
+ }
+
+ TimeStamp& StartTime() { return mStartTime; }
+
+ bool& InSeconds() { return mInSeconds; }
+
+ /**
+ * Note that these values will want to be read from the
+ * BackgroundHangAnnotator thread. Callers should take a lock
+ * on Timers::mBHRAnnotationTimers before calling this.
+ */
+ void SetBHRAnnotation(const nsAString& aBHRAnnotationKey,
+ const nsACString& aBHRAnnotationValue) {
+ mBHRAnnotationKey = aBHRAnnotationKey;
+ mBHRAnnotationValue = aBHRAnnotationValue;
+ }
+
+ const nsString& GetBHRAnnotationKey() const { return mBHRAnnotationKey; }
+ const nsCString& GetBHRAnnotationValue() const { return mBHRAnnotationValue; }
+
+ private:
+ ~Timer() = default;
+ TimeStamp mStartTime{};
+ nsString mBHRAnnotationKey;
+ nsCString mBHRAnnotationValue;
+ bool mInSeconds;
+};
+
+#define TIMER_KEYS_IID \
+ { \
+ 0xef707178, 0x1544, 0x46e2, { \
+ 0xa3, 0xf5, 0x98, 0x38, 0xba, 0x60, 0xfd, 0x8f \
+ } \
+ }
+
+class TimerKeys final : public nsISupports {
+ public:
+ NS_DECL_ISUPPORTS
+ NS_DECLARE_STATIC_IID_ACCESSOR(TIMER_KEYS_IID)
+
+ Timer* Get(const nsAString& aKey, bool aCreate = true);
+
+ already_AddRefed<Timer> GetAndDelete(const nsAString& aKey) {
+ RefPtr<Timer> timer;
+ mTimers.Remove(aKey, getter_AddRefs(timer));
+ return timer.forget();
+ }
+
+ bool Delete(const nsAString& aKey) { return mTimers.Remove(aKey); }
+
+ private:
+ ~TimerKeys() = default;
+
+ nsRefPtrHashtable<nsStringHashKey, Timer> mTimers;
+};
+
+NS_DEFINE_STATIC_IID_ACCESSOR(TimerKeys, TIMER_KEYS_IID)
+
+NS_IMPL_ISUPPORTS(TimerKeys, TimerKeys)
+
+Timer* TimerKeys::Get(const nsAString& aKey, bool aCreate) {
+ if (aCreate) {
+ return mTimers.GetOrInsertNew(aKey);
+ }
+ return mTimers.GetWeak(aKey);
+}
+
+class Timers final : public BackgroundHangAnnotator {
+ public:
+ Timers();
+
+ static Timers& Singleton();
+
+ NS_INLINE_DECL_REFCOUNTING(Timers)
+
+ JSObject* Get(JSContext* aCx, const nsAString& aHistogram,
+ bool aCreate = true);
+
+ TimerKeys* Get(JSContext* aCx, const nsAString& aHistogram,
+ JS::Handle<JSObject*> aObj, bool aCreate = true);
+
+ Timer* Get(JSContext* aCx, const nsAString& aHistogram,
+ JS::Handle<JSObject*> aObj, const nsAString& aKey,
+ bool aCreate = true);
+
+ already_AddRefed<Timer> GetAndDelete(JSContext* aCx,
+ const nsAString& aHistogram,
+ JS::Handle<JSObject*> aObj,
+ const nsAString& aKey);
+
+ bool Delete(JSContext* aCx, const nsAString& aHistogram,
+ JS::Handle<JSObject*> aObj, const nsAString& aKey);
+
+ int32_t TimeElapsed(JSContext* aCx, const nsAString& aHistogram,
+ JS::Handle<JSObject*> aObj, const nsAString& aKey,
+ bool aCanceledOkay = false);
+
+ bool Start(JSContext* aCx, const nsAString& aHistogram,
+ JS::Handle<JSObject*> aObj, const nsAString& aKey,
+ bool aInSeconds = false);
+
+ int32_t Finish(JSContext* aCx, const nsAString& aHistogram,
+ JS::Handle<JSObject*> aObj, const nsAString& aKey,
+ bool aCanceledOkay = false);
+
+ bool& SuppressErrors() { return mSuppressErrors; }
+
+ bool StartUserInteraction(JSContext* aCx, const nsAString& aUserInteraction,
+ const nsACString& aValue,
+ JS::Handle<JSObject*> aObj);
+ bool RunningUserInteraction(JSContext* aCx, const nsAString& aUserInteraction,
+ JS::Handle<JSObject*> aObj);
+ bool UpdateUserInteraction(JSContext* aCx, const nsAString& aUserInteraction,
+ const nsACString& aValue,
+ JS::Handle<JSObject*> aObj);
+ bool FinishUserInteraction(JSContext* aCx, const nsAString& aUserInteraction,
+ JS::Handle<JSObject*> aObj,
+ const dom::Optional<nsACString>& aAdditionalText);
+ bool CancelUserInteraction(JSContext* aCx, const nsAString& aUserInteraction,
+ JS::Handle<JSObject*> aObj);
+
+ void AnnotateHang(BackgroundHangAnnotations& aAnnotations) final;
+
+ private:
+ ~Timers();
+
+ JS::PersistentRooted<JSObject*> mTimers;
+ DataMutex<mozilla::LinkedList<RefPtr<Timer>>> mBHRAnnotationTimers;
+ bool mSuppressErrors = false;
+
+ static StaticRefPtr<Timers> sSingleton;
+};
+
+StaticRefPtr<Timers> Timers::sSingleton;
+
+/* static */ Timers& Timers::Singleton() {
+ if (!sSingleton) {
+ sSingleton = new Timers();
+ ClearOnShutdown(&sSingleton);
+ }
+ return *sSingleton;
+}
+
+Timers::Timers()
+ : mTimers(dom::RootingCx()), mBHRAnnotationTimers("BHRAnnotationTimers") {
+ AutoJSAPI jsapi;
+ MOZ_ALWAYS_TRUE(jsapi.Init(xpc::PrivilegedJunkScope()));
+
+ mTimers = JS::NewMapObject(jsapi.cx());
+ MOZ_RELEASE_ASSERT(mTimers);
+
+ BackgroundHangMonitor::RegisterAnnotator(*this);
+}
+
+Timers::~Timers() {
+ // We use a scope here to prevent a deadlock with the mutex that locks
+ // inside of ::UnregisterAnnotator.
+ {
+ auto annotationTimers = mBHRAnnotationTimers.Lock();
+ annotationTimers->clear();
+ }
+ BackgroundHangMonitor::UnregisterAnnotator(*this);
+}
+
+JSObject* Timers::Get(JSContext* aCx, const nsAString& aHistogram,
+ bool aCreate) {
+ JSAutoRealm ar(aCx, mTimers);
+
+ JS::Rooted<JS::Value> histogram(aCx);
+ JS::Rooted<JS::Value> objs(aCx);
+
+ if (!xpc::NonVoidStringToJsval(aCx, aHistogram, &histogram) ||
+ !JS::MapGet(aCx, mTimers, histogram, &objs)) {
+ return nullptr;
+ }
+ if (!objs.isObject()) {
+ if (aCreate) {
+ objs = JS::ObjectOrNullValue(JS::NewWeakMapObject(aCx));
+ }
+ if (!objs.isObject() || !JS::MapSet(aCx, mTimers, histogram, objs)) {
+ return nullptr;
+ }
+ }
+
+ return &objs.toObject();
+}
+
+TimerKeys* Timers::Get(JSContext* aCx, const nsAString& aHistogram,
+ JS::Handle<JSObject*> aObj, bool aCreate) {
+ JSAutoRealm ar(aCx, mTimers);
+
+ JS::Rooted<JSObject*> objs(aCx, Get(aCx, aHistogram, aCreate));
+ if (!objs) {
+ return nullptr;
+ }
+
+ // If no object is passed, use mTimers as a stand-in for a null object
+ // (which cannot be used as a weak map key).
+ JS::Rooted<JSObject*> obj(aCx, aObj ? aObj : mTimers);
+ if (!JS_WrapObject(aCx, &obj)) {
+ return nullptr;
+ }
+
+ RefPtr<TimerKeys> keys;
+ JS::Rooted<JS::Value> keysObj(aCx);
+ JS::Rooted<JS::Value> objVal(aCx, JS::ObjectValue(*obj));
+ if (!JS::GetWeakMapEntry(aCx, objs, objVal, &keysObj)) {
+ return nullptr;
+ }
+ if (!keysObj.isObject()) {
+ if (aCreate) {
+ keys = new TimerKeys();
+ Unused << nsContentUtils::WrapNative(aCx, keys, &keysObj);
+ }
+ if (!keysObj.isObject() ||
+ !JS::SetWeakMapEntry(aCx, objs, objVal, keysObj)) {
+ return nullptr;
+ }
+ }
+
+ keys = do_QueryReflector(keysObj);
+ return keys;
+}
+
+Timer* Timers::Get(JSContext* aCx, const nsAString& aHistogram,
+ JS::Handle<JSObject*> aObj, const nsAString& aKey,
+ bool aCreate) {
+ if (RefPtr<TimerKeys> keys = Get(aCx, aHistogram, aObj, aCreate)) {
+ return keys->Get(aKey, aCreate);
+ }
+ return nullptr;
+}
+
+already_AddRefed<Timer> Timers::GetAndDelete(JSContext* aCx,
+ const nsAString& aHistogram,
+ JS::Handle<JSObject*> aObj,
+ const nsAString& aKey) {
+ if (RefPtr<TimerKeys> keys = Get(aCx, aHistogram, aObj, false)) {
+ return keys->GetAndDelete(aKey);
+ }
+ return nullptr;
+}
+
+bool Timers::Delete(JSContext* aCx, const nsAString& aHistogram,
+ JS::Handle<JSObject*> aObj, const nsAString& aKey) {
+ if (RefPtr<TimerKeys> keys = Get(aCx, aHistogram, aObj, false)) {
+ return keys->Delete(aKey);
+ }
+ return false;
+}
+
+int32_t Timers::TimeElapsed(JSContext* aCx, const nsAString& aHistogram,
+ JS::Handle<JSObject*> aObj, const nsAString& aKey,
+ bool aCanceledOkay) {
+ RefPtr<Timer> timer = Get(aCx, aHistogram, aObj, aKey, false);
+ if (!timer) {
+ if (!aCanceledOkay && !mSuppressErrors) {
+ LogError(aCx, nsPrintfCString(
+ "TelemetryStopwatch: requesting elapsed time for "
+ "nonexisting stopwatch. Histogram: \"%s\", key: \"%s\"",
+ NS_ConvertUTF16toUTF8(aHistogram).get(),
+ NS_ConvertUTF16toUTF8(aKey).get()));
+ }
+ return -1;
+ }
+
+ return timer->Elapsed();
+}
+
+bool Timers::Start(JSContext* aCx, const nsAString& aHistogram,
+ JS::Handle<JSObject*> aObj, const nsAString& aKey,
+ bool aInSeconds) {
+ if (RefPtr<Timer> timer = Get(aCx, aHistogram, aObj, aKey)) {
+ if (timer->Started()) {
+ if (!mSuppressErrors) {
+ LogError(aCx,
+ nsPrintfCString(
+ "TelemetryStopwatch: key \"%s\" was already initialized",
+ NS_ConvertUTF16toUTF8(aHistogram).get()));
+ }
+ Delete(aCx, aHistogram, aObj, aKey);
+ } else {
+ timer->Start(aInSeconds);
+ return true;
+ }
+ }
+ return false;
+}
+
+int32_t Timers::Finish(JSContext* aCx, const nsAString& aHistogram,
+ JS::Handle<JSObject*> aObj, const nsAString& aKey,
+ bool aCanceledOkay) {
+ RefPtr<Timer> timer = GetAndDelete(aCx, aHistogram, aObj, aKey);
+ if (!timer) {
+ if (!aCanceledOkay && !mSuppressErrors) {
+ LogError(aCx, nsPrintfCString(
+ "TelemetryStopwatch: finishing nonexisting stopwatch. "
+ "Histogram: \"%s\", key: \"%s\"",
+ NS_ConvertUTF16toUTF8(aHistogram).get(),
+ NS_ConvertUTF16toUTF8(aKey).get()));
+ }
+ return -1;
+ }
+
+ int32_t delta = timer->Elapsed();
+ NS_ConvertUTF16toUTF8 histogram(aHistogram);
+ nsresult rv;
+ if (!aKey.IsVoid()) {
+ NS_ConvertUTF16toUTF8 key(aKey);
+ rv = TelemetryHistogram::Accumulate(histogram.get(), key, delta);
+ } else {
+ rv = TelemetryHistogram::Accumulate(histogram.get(), delta);
+ }
+ if (profiler_thread_is_being_profiled_for_markers()) {
+ nsCString markerText = histogram;
+ if (!aKey.IsVoid()) {
+ markerText.AppendLiteral(":");
+ markerText.Append(NS_ConvertUTF16toUTF8(aKey));
+ }
+ PROFILER_MARKER_TEXT("TelemetryStopwatch", OTHER,
+ MarkerTiming::IntervalUntilNowFrom(timer->StartTime()),
+ markerText);
+ }
+ if (NS_FAILED(rv) && rv != NS_ERROR_NOT_AVAILABLE && !mSuppressErrors) {
+ LogError(aCx, nsPrintfCString(
+ "TelemetryStopwatch: failed to update the Histogram "
+ "\"%s\", using key: \"%s\"",
+ NS_ConvertUTF16toUTF8(aHistogram).get(),
+ NS_ConvertUTF16toUTF8(aKey).get()));
+ }
+ return NS_SUCCEEDED(rv) ? delta : -1;
+}
+
+bool Timers::StartUserInteraction(JSContext* aCx,
+ const nsAString& aUserInteraction,
+ const nsACString& aValue,
+ JS::Handle<JSObject*> aObj) {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ // Ensure that this ID maps to a UserInteraction that can be recorded
+ // for this product.
+ if (!TelemetryUserInteraction::CanRecord(aUserInteraction)) {
+ if (!mSuppressErrors) {
+ LogError(aCx, nsPrintfCString(
+ "UserInteraction with name \"%s\" cannot be recorded.",
+ NS_ConvertUTF16toUTF8(aUserInteraction).get()));
+ }
+ return false;
+ }
+
+ if (aValue.Length() > USER_INTERACTION_VALUE_MAX_LENGTH) {
+ if (!mSuppressErrors) {
+ LogError(aCx,
+ nsPrintfCString(
+ "UserInteraction with name \"%s\" cannot be recorded with"
+ "a value of length greater than %d (%s)",
+ NS_ConvertUTF16toUTF8(aUserInteraction).get(),
+ USER_INTERACTION_VALUE_MAX_LENGTH,
+ PromiseFlatCString(aValue).get()));
+ }
+ return false;
+ }
+
+ if (RefPtr<Timer> timer = Get(aCx, aUserInteraction, aObj, VoidString())) {
+ auto annotationTimers = mBHRAnnotationTimers.Lock();
+
+ if (timer->Started()) {
+ if (!mSuppressErrors) {
+ LogError(aCx,
+ nsPrintfCString(
+ "UserInteraction with name \"%s\" was already initialized",
+ NS_ConvertUTF16toUTF8(aUserInteraction).get()));
+ }
+ timer->removeFrom(*annotationTimers);
+ Delete(aCx, aUserInteraction, aObj, VoidString());
+ timer = Get(aCx, aUserInteraction, aObj, VoidString());
+
+ nsAutoString clobberText(aUserInteraction);
+ clobberText.AppendLiteral(u" (clobbered)");
+ timer->SetBHRAnnotation(clobberText, aValue);
+ } else {
+ timer->SetBHRAnnotation(aUserInteraction, aValue);
+ }
+
+ annotationTimers->insertBack(timer);
+ timer->Start(false);
+ return true;
+ }
+ return false;
+}
+
+bool Timers::RunningUserInteraction(JSContext* aCx,
+ const nsAString& aUserInteraction,
+ JS::Handle<JSObject*> aObj) {
+ if (RefPtr<Timer> timer =
+ Get(aCx, aUserInteraction, aObj, VoidString(), false /* aCreate */)) {
+ return timer->Started();
+ }
+ return false;
+}
+
+bool Timers::UpdateUserInteraction(JSContext* aCx,
+ const nsAString& aUserInteraction,
+ const nsACString& aValue,
+ JS::Handle<JSObject*> aObj) {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ // Ensure that this ID maps to a UserInteraction that can be recorded
+ // for this product.
+ if (!TelemetryUserInteraction::CanRecord(aUserInteraction)) {
+ if (!mSuppressErrors) {
+ LogError(aCx, nsPrintfCString(
+ "UserInteraction with name \"%s\" cannot be recorded.",
+ NS_ConvertUTF16toUTF8(aUserInteraction).get()));
+ }
+ return false;
+ }
+
+ auto lock = mBHRAnnotationTimers.Lock();
+ if (RefPtr<Timer> timer = Get(aCx, aUserInteraction, aObj, VoidString())) {
+ if (!timer->Started()) {
+ if (!mSuppressErrors) {
+ LogError(aCx, nsPrintfCString(
+ "UserInteraction with id \"%s\" was not initialized",
+ NS_ConvertUTF16toUTF8(aUserInteraction).get()));
+ }
+ return false;
+ }
+ timer->SetBHRAnnotation(aUserInteraction, aValue);
+ return true;
+ }
+ return false;
+}
+
+bool Timers::FinishUserInteraction(
+ JSContext* aCx, const nsAString& aUserInteraction,
+ JS::Handle<JSObject*> aObj,
+ const dom::Optional<nsACString>& aAdditionalText) {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ // Ensure that this ID maps to a UserInteraction that can be recorded
+ // for this product.
+ if (!TelemetryUserInteraction::CanRecord(aUserInteraction)) {
+ if (!mSuppressErrors) {
+ LogError(aCx, nsPrintfCString(
+ "UserInteraction with id \"%s\" cannot be recorded.",
+ NS_ConvertUTF16toUTF8(aUserInteraction).get()));
+ }
+ return false;
+ }
+
+ RefPtr<Timer> timer = GetAndDelete(aCx, aUserInteraction, aObj, VoidString());
+ if (!timer) {
+ if (!mSuppressErrors) {
+ LogError(aCx, nsPrintfCString(
+ "UserInteraction: finishing nonexisting stopwatch. "
+ "name: \"%s\"",
+ NS_ConvertUTF16toUTF8(aUserInteraction).get()));
+ }
+ return false;
+ }
+
+ if (profiler_thread_is_being_profiled_for_markers()) {
+ nsAutoCString markerText(timer->GetBHRAnnotationValue());
+ if (aAdditionalText.WasPassed()) {
+ markerText.Append(",");
+ markerText.Append(aAdditionalText.Value());
+ }
+
+ PROFILER_MARKER_TEXT(NS_ConvertUTF16toUTF8(aUserInteraction), OTHER,
+ MarkerTiming::IntervalUntilNowFrom(timer->StartTime()),
+ markerText);
+ }
+
+ // The Timer will be held alive by the RefPtr that's still in the LinkedList,
+ // so the automatic removal from the LinkedList from the LinkedListElement
+ // destructor will not occur. We must remove it manually from the LinkedList
+ // instead.
+ {
+ auto annotationTimers = mBHRAnnotationTimers.Lock();
+ timer->removeFrom(*annotationTimers);
+ }
+
+ return true;
+}
+
+bool Timers::CancelUserInteraction(JSContext* aCx,
+ const nsAString& aUserInteraction,
+ JS::Handle<JSObject*> aObj) {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ // Ensure that this ID maps to a UserInteraction that can be recorded
+ // for this product.
+ if (!TelemetryUserInteraction::CanRecord(aUserInteraction)) {
+ if (!mSuppressErrors) {
+ LogError(aCx, nsPrintfCString(
+ "UserInteraction with id \"%s\" cannot be recorded.",
+ NS_ConvertUTF16toUTF8(aUserInteraction).get()));
+ }
+ return false;
+ }
+
+ RefPtr<Timer> timer = GetAndDelete(aCx, aUserInteraction, aObj, VoidString());
+ if (!timer) {
+ if (!mSuppressErrors) {
+ LogError(aCx, nsPrintfCString(
+ "UserInteraction: cancelling nonexisting stopwatch. "
+ "name: \"%s\"",
+ NS_ConvertUTF16toUTF8(aUserInteraction).get()));
+ }
+ return false;
+ }
+
+ // The Timer will be held alive by the RefPtr that's still in the LinkedList,
+ // so the automatic removal from the LinkedList from the LinkedListElement
+ // destructor will not occur. We must remove it manually from the LinkedList
+ // instead.
+ {
+ auto annotationTimers = mBHRAnnotationTimers.Lock();
+ timer->removeFrom(*annotationTimers);
+ }
+
+ return true;
+}
+
+void Timers::AnnotateHang(mozilla::BackgroundHangAnnotations& aAnnotations) {
+ auto annotationTimers = mBHRAnnotationTimers.Lock();
+ for (Timer* bhrAnnotationTimer : *annotationTimers) {
+ aAnnotations.AddAnnotation(bhrAnnotationTimer->GetBHRAnnotationKey(),
+ bhrAnnotationTimer->GetBHRAnnotationValue());
+ }
+}
+
+/* static */
+bool Stopwatch::Start(const dom::GlobalObject& aGlobal,
+ const nsAString& aHistogram, JS::Handle<JSObject*> aObj,
+ const dom::TelemetryStopwatchOptions& aOptions) {
+ return StartKeyed(aGlobal, aHistogram, VoidString(), aObj, aOptions);
+}
+/* static */
+bool Stopwatch::StartKeyed(const dom::GlobalObject& aGlobal,
+ const nsAString& aHistogram, const nsAString& aKey,
+ JS::Handle<JSObject*> aObj,
+ const dom::TelemetryStopwatchOptions& aOptions) {
+ return Timers::Singleton().Start(aGlobal.Context(), aHistogram, aObj, aKey,
+ aOptions.mInSeconds);
+}
+
+/* static */
+bool Stopwatch::Running(const dom::GlobalObject& aGlobal,
+ const nsAString& aHistogram,
+ JS::Handle<JSObject*> aObj) {
+ return RunningKeyed(aGlobal, aHistogram, VoidString(), aObj);
+}
+
+/* static */
+bool Stopwatch::RunningKeyed(const dom::GlobalObject& aGlobal,
+ const nsAString& aHistogram, const nsAString& aKey,
+ JS::Handle<JSObject*> aObj) {
+ return TimeElapsedKeyed(aGlobal, aHistogram, aKey, aObj, true) != -1;
+}
+
+/* static */
+int32_t Stopwatch::TimeElapsed(const dom::GlobalObject& aGlobal,
+ const nsAString& aHistogram,
+ JS::Handle<JSObject*> aObj, bool aCanceledOkay) {
+ return TimeElapsedKeyed(aGlobal, aHistogram, VoidString(), aObj,
+ aCanceledOkay);
+}
+
+/* static */
+int32_t Stopwatch::TimeElapsedKeyed(const dom::GlobalObject& aGlobal,
+ const nsAString& aHistogram,
+ const nsAString& aKey,
+ JS::Handle<JSObject*> aObj,
+ bool aCanceledOkay) {
+ return Timers::Singleton().TimeElapsed(aGlobal.Context(), aHistogram, aObj,
+ aKey, aCanceledOkay);
+}
+
+/* static */
+bool Stopwatch::Finish(const dom::GlobalObject& aGlobal,
+ const nsAString& aHistogram, JS::Handle<JSObject*> aObj,
+ bool aCanceledOkay) {
+ return FinishKeyed(aGlobal, aHistogram, VoidString(), aObj, aCanceledOkay);
+}
+
+/* static */
+bool Stopwatch::FinishKeyed(const dom::GlobalObject& aGlobal,
+ const nsAString& aHistogram, const nsAString& aKey,
+ JS::Handle<JSObject*> aObj, bool aCanceledOkay) {
+ return Timers::Singleton().Finish(aGlobal.Context(), aHistogram, aObj, aKey,
+ aCanceledOkay) != -1;
+}
+
+/* static */
+bool Stopwatch::Cancel(const dom::GlobalObject& aGlobal,
+ const nsAString& aHistogram,
+ JS::Handle<JSObject*> aObj) {
+ return CancelKeyed(aGlobal, aHistogram, VoidString(), aObj);
+}
+
+/* static */
+bool Stopwatch::CancelKeyed(const dom::GlobalObject& aGlobal,
+ const nsAString& aHistogram, const nsAString& aKey,
+ JS::Handle<JSObject*> aObj) {
+ return Timers::Singleton().Delete(aGlobal.Context(), aHistogram, aObj, aKey);
+}
+
+/* static */
+void Stopwatch::SetTestModeEnabled(const dom::GlobalObject& aGlobal,
+ bool aTesting) {
+ Timers::Singleton().SuppressErrors() = aTesting;
+}
+
+/* static */
+bool UserInteractionStopwatch::Start(const dom::GlobalObject& aGlobal,
+ const nsAString& aUserInteraction,
+ const nsACString& aValue,
+ JS::Handle<JSObject*> aObj) {
+ if (!NS_IsMainThread()) {
+ return false;
+ }
+ return Timers::Singleton().StartUserInteraction(
+ aGlobal.Context(), aUserInteraction, aValue, aObj);
+}
+
+/* static */
+bool UserInteractionStopwatch::Running(const dom::GlobalObject& aGlobal,
+ const nsAString& aUserInteraction,
+ JS::Handle<JSObject*> aObj) {
+ if (!NS_IsMainThread()) {
+ return false;
+ }
+ return Timers::Singleton().RunningUserInteraction(aGlobal.Context(),
+ aUserInteraction, aObj);
+}
+
+/* static */
+bool UserInteractionStopwatch::Update(const dom::GlobalObject& aGlobal,
+ const nsAString& aUserInteraction,
+ const nsACString& aValue,
+ JS::Handle<JSObject*> aObj) {
+ if (!NS_IsMainThread()) {
+ return false;
+ }
+ return Timers::Singleton().UpdateUserInteraction(
+ aGlobal.Context(), aUserInteraction, aValue, aObj);
+}
+
+/* static */
+bool UserInteractionStopwatch::Cancel(const dom::GlobalObject& aGlobal,
+ const nsAString& aUserInteraction,
+ JS::Handle<JSObject*> aObj) {
+ if (!NS_IsMainThread()) {
+ return false;
+ }
+ return Timers::Singleton().CancelUserInteraction(aGlobal.Context(),
+ aUserInteraction, aObj);
+}
+
+/* static */
+bool UserInteractionStopwatch::Finish(
+ const dom::GlobalObject& aGlobal, const nsAString& aUserInteraction,
+ JS::Handle<JSObject*> aObj,
+ const dom::Optional<nsACString>& aAdditionalText) {
+ if (!NS_IsMainThread()) {
+ return false;
+ }
+ return Timers::Singleton().FinishUserInteraction(
+ aGlobal.Context(), aUserInteraction, aObj, aAdditionalText);
+}
+
+} // namespace mozilla::telemetry
diff --git a/toolkit/components/telemetry/core/Stopwatch.h b/toolkit/components/telemetry/core/Stopwatch.h
new file mode 100644
index 0000000000..070872e250
--- /dev/null
+++ b/toolkit/components/telemetry/core/Stopwatch.h
@@ -0,0 +1,84 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2; -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef Stopwatch_h__
+#define Stopwatch_h__
+
+#include "mozilla/dom/TelemetryStopwatchBinding.h"
+
+namespace mozilla {
+namespace telemetry {
+
+class Stopwatch {
+ using GlobalObject = mozilla::dom::GlobalObject;
+
+ public:
+ static bool Start(const GlobalObject& global, const nsAString& histogram,
+ JS::Handle<JSObject*> obj,
+ const dom::TelemetryStopwatchOptions& options);
+
+ static bool Running(const GlobalObject& global, const nsAString& histogram,
+ JS::Handle<JSObject*> obj);
+
+ static bool Cancel(const GlobalObject& global, const nsAString& histogram,
+ JS::Handle<JSObject*> obj);
+
+ static int32_t TimeElapsed(const GlobalObject& global,
+ const nsAString& histogram,
+ JS::Handle<JSObject*> obj, bool canceledOkay);
+
+ static bool Finish(const GlobalObject& global, const nsAString& histogram,
+ JS::Handle<JSObject*> obj, bool canceledOkay);
+
+ static bool StartKeyed(const GlobalObject& global, const nsAString& histogram,
+ const nsAString& key, JS::Handle<JSObject*> obj,
+ const dom::TelemetryStopwatchOptions& options);
+
+ static bool RunningKeyed(const GlobalObject& global,
+ const nsAString& histogram, const nsAString& key,
+ JS::Handle<JSObject*> obj);
+
+ static bool CancelKeyed(const GlobalObject& global,
+ const nsAString& histogram, const nsAString& key,
+ JS::Handle<JSObject*> obj);
+
+ static int32_t TimeElapsedKeyed(const GlobalObject& global,
+ const nsAString& histogram,
+ const nsAString& key,
+ JS::Handle<JSObject*> obj, bool canceledOkay);
+
+ static bool FinishKeyed(const GlobalObject& global,
+ const nsAString& histogram, const nsAString& key,
+ JS::Handle<JSObject*> obj, bool canceledOkay);
+
+ static void SetTestModeEnabled(const GlobalObject& global, bool testing);
+};
+
+class UserInteractionStopwatch {
+ using GlobalObject = mozilla::dom::GlobalObject;
+
+ public:
+ static bool Start(const GlobalObject& aGlobal,
+ const nsAString& aUserInteraction, const nsACString& aValue,
+ JS::Handle<JSObject*> aObj);
+ static bool Running(const GlobalObject& aGlobal,
+ const nsAString& aUserInteraction,
+ JS::Handle<JSObject*> aObj);
+ static bool Update(const GlobalObject& aGlobal,
+ const nsAString& aUserInteraction,
+ const nsACString& aValue, JS::Handle<JSObject*> aObj);
+ static bool Cancel(const GlobalObject& aGlobal,
+ const nsAString& aUserInteraction,
+ JS::Handle<JSObject*> aObj);
+ static bool Finish(const GlobalObject& aGlobal,
+ const nsAString& aUserInteraction,
+ JS::Handle<JSObject*> aObj,
+ const dom::Optional<nsACString>& aAdditionalText);
+};
+
+} // namespace telemetry
+} // namespace mozilla
+
+#endif // Stopwatch_h__
diff --git a/toolkit/components/telemetry/core/Telemetry.cpp b/toolkit/components/telemetry/core/Telemetry.cpp
new file mode 100644
index 0000000000..a0effb02bb
--- /dev/null
+++ b/toolkit/components/telemetry/core/Telemetry.cpp
@@ -0,0 +1,2035 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "Telemetry.h"
+
+#include <algorithm>
+#include <prio.h>
+#include <prproces.h>
+#if defined(XP_UNIX) && !defined(XP_DARWIN)
+# include <time.h>
+#else
+# include <chrono>
+#endif
+#include "base/pickle.h"
+#include "base/process_util.h"
+#if defined(MOZ_TELEMETRY_GECKOVIEW)
+# include "geckoview/TelemetryGeckoViewPersistence.h"
+#endif
+#include "ipc/TelemetryIPCAccumulator.h"
+#include "jsapi.h"
+#include "jsfriendapi.h"
+#include "js/Array.h" // JS::NewArrayObject
+#include "js/GCAPI.h"
+#include "js/PropertyAndElement.h" // JS_DefineElement, JS_DefineProperty
+#include "mozilla/dom/ToJSValue.h"
+#include "mozilla/dom/Promise.h"
+#include "mozilla/Atomics.h"
+#include "mozilla/Attributes.h"
+#include "mozilla/BackgroundHangMonitor.h"
+#ifdef MOZ_BACKGROUNDTASKS
+# include "mozilla/BackgroundTasks.h"
+#endif
+#include "mozilla/Components.h"
+#include "mozilla/DataMutex.h"
+#include "mozilla/DebugOnly.h"
+#include "mozilla/FStream.h"
+#include "mozilla/IOInterposer.h"
+#include "mozilla/Likely.h"
+#include "mozilla/MathAlgorithms.h"
+#include "mozilla/MemoryReporting.h"
+#include "mozilla/MemoryTelemetry.h"
+#include "mozilla/ModuleUtils.h"
+#include "mozilla/Mutex.h"
+#include "mozilla/PoisonIOInterposer.h"
+#include "mozilla/Preferences.h"
+#include "mozilla/StartupTimeline.h"
+#include "mozilla/StaticPtr.h"
+#include "mozilla/Unused.h"
+#if defined(XP_WIN)
+# include "mozilla/WinDllServices.h"
+#endif
+#include "nsAppDirectoryServiceDefs.h"
+#include "nsBaseHashtable.h"
+#include "nsClassHashtable.h"
+#include "nsCOMArray.h"
+#include "nsCOMPtr.h"
+#include "nsTHashMap.h"
+#include "nsHashKeys.h"
+#include "nsIDirectoryEnumerator.h"
+#include "nsDirectoryServiceDefs.h"
+#include "nsIFileStreams.h"
+#include "nsIMemoryReporter.h"
+#include "nsISeekableStream.h"
+#include "nsITelemetry.h"
+#if defined(XP_WIN)
+# include "other/UntrustedModules.h"
+#endif
+#include "nsJSUtils.h"
+#include "nsLocalFile.h"
+#include "nsNativeCharsetUtils.h"
+#include "nsNetCID.h"
+#include "nsNetUtil.h"
+#include "nsProxyRelease.h"
+#include "nsReadableUtils.h"
+#include "nsString.h"
+#include "nsTHashtable.h"
+#include "nsThreadUtils.h"
+#if defined(XP_WIN)
+# include "nsUnicharUtils.h"
+#endif
+#include "nsVersionComparator.h"
+#include "nsXPCOMCIDInternal.h"
+#include "other/CombinedStacks.h"
+#include "other/TelemetryIOInterposeObserver.h"
+#include "TelemetryCommon.h"
+#include "TelemetryEvent.h"
+#include "TelemetryHistogram.h"
+#include "TelemetryScalar.h"
+#include "TelemetryUserInteraction.h"
+
+namespace {
+
+using namespace mozilla;
+using mozilla::dom::AutoJSAPI;
+using mozilla::dom::Promise;
+using mozilla::Telemetry::CombinedStacks;
+using mozilla::Telemetry::EventExtraEntry;
+using mozilla::Telemetry::TelemetryIOInterposeObserver;
+using Telemetry::Common::AutoHashtable;
+using Telemetry::Common::GetCurrentProduct;
+using Telemetry::Common::StringHashSet;
+using Telemetry::Common::SupportedProduct;
+using Telemetry::Common::ToJSString;
+
+// This is not a member of TelemetryImpl because we want to record I/O during
+// startup.
+StaticAutoPtr<TelemetryIOInterposeObserver> sTelemetryIOObserver;
+
+void ClearIOReporting() {
+ if (!sTelemetryIOObserver) {
+ return;
+ }
+ IOInterposer::Unregister(IOInterposeObserver::OpAllWithStaging,
+ sTelemetryIOObserver);
+ sTelemetryIOObserver = nullptr;
+}
+
+class TelemetryImpl final : public nsITelemetry, public nsIMemoryReporter {
+ NS_DECL_THREADSAFE_ISUPPORTS
+ NS_DECL_NSITELEMETRY
+ NS_DECL_NSIMEMORYREPORTER
+
+ public:
+ void InitMemoryReporter();
+
+ static already_AddRefed<nsITelemetry> CreateTelemetryInstance();
+ static void ShutdownTelemetry();
+ static void RecordSlowStatement(const nsACString& sql,
+ const nsACString& dbName, uint32_t delay);
+ struct Stat {
+ uint32_t hitCount;
+ uint32_t totalTime;
+ };
+ struct StmtStats {
+ struct Stat mainThread;
+ struct Stat otherThreads;
+ };
+ typedef nsBaseHashtableET<nsCStringHashKey, StmtStats> SlowSQLEntryType;
+
+ static void RecordIceCandidates(const uint32_t iceCandidateBitmask,
+ const bool success);
+ static bool CanRecordBase();
+ static bool CanRecordExtended();
+ static bool CanRecordReleaseData();
+ static bool CanRecordPrereleaseData();
+
+ private:
+ TelemetryImpl();
+ ~TelemetryImpl();
+
+ static nsCString SanitizeSQL(const nsACString& sql);
+
+ enum SanitizedState { Sanitized, Unsanitized };
+
+ static void StoreSlowSQL(const nsACString& offender, uint32_t delay,
+ SanitizedState state);
+
+ static bool ReflectMainThreadSQL(SlowSQLEntryType* entry, JSContext* cx,
+ JS::Handle<JSObject*> obj);
+ static bool ReflectOtherThreadsSQL(SlowSQLEntryType* entry, JSContext* cx,
+ JS::Handle<JSObject*> obj);
+ static bool ReflectSQL(const SlowSQLEntryType* entry, const Stat* stat,
+ JSContext* cx, JS::Handle<JSObject*> obj);
+
+ bool AddSQLInfo(JSContext* cx, JS::Handle<JSObject*> rootObj, bool mainThread,
+ bool privateSQL);
+ bool GetSQLStats(JSContext* cx, JS::MutableHandle<JS::Value> ret,
+ bool includePrivateSql);
+
+ void ReadLateWritesStacks(nsIFile* aProfileDir);
+
+ static StaticDataMutex<TelemetryImpl*> sTelemetry;
+ AutoHashtable<SlowSQLEntryType> mPrivateSQL;
+ AutoHashtable<SlowSQLEntryType> mSanitizedSQL;
+ Mutex mHashMutex MOZ_UNANNOTATED;
+ Atomic<bool, SequentiallyConsistent> mCanRecordBase;
+ Atomic<bool, SequentiallyConsistent> mCanRecordExtended;
+
+ CombinedStacks
+ mLateWritesStacks; // This is collected out of the main thread.
+ bool mCachedTelemetryData;
+ uint32_t mLastShutdownTime;
+ uint32_t mFailedLockCount;
+ nsCOMArray<nsIFetchTelemetryDataCallback> mCallbacks;
+ friend class nsFetchTelemetryData;
+};
+
+StaticDataMutex<TelemetryImpl*> TelemetryImpl::sTelemetry(nullptr, nullptr);
+
+MOZ_DEFINE_MALLOC_SIZE_OF(TelemetryMallocSizeOf)
+
+NS_IMETHODIMP
+TelemetryImpl::CollectReports(nsIHandleReportCallback* aHandleReport,
+ nsISupports* aData, bool aAnonymize) {
+ mozilla::MallocSizeOf aMallocSizeOf = TelemetryMallocSizeOf;
+
+#define COLLECT_REPORT(name, size, desc) \
+ MOZ_COLLECT_REPORT(name, KIND_HEAP, UNITS_BYTES, size, desc)
+
+ COLLECT_REPORT("explicit/telemetry/impl", aMallocSizeOf(this),
+ "Memory used by the Telemetry core implemenation");
+
+ COLLECT_REPORT(
+ "explicit/telemetry/scalar/shallow",
+ TelemetryScalar::GetMapShallowSizesOfExcludingThis(aMallocSizeOf),
+ "Memory used by the Telemetry Scalar implemenation");
+
+ { // Scope for mHashMutex lock
+ MutexAutoLock lock(mHashMutex);
+ COLLECT_REPORT("explicit/telemetry/PrivateSQL",
+ mPrivateSQL.SizeOfExcludingThis(aMallocSizeOf),
+ "Memory used by the PrivateSQL Telemetry");
+
+ COLLECT_REPORT("explicit/telemetry/SanitizedSQL",
+ mSanitizedSQL.SizeOfExcludingThis(aMallocSizeOf),
+ "Memory used by the SanitizedSQL Telemetry");
+ }
+
+ if (sTelemetryIOObserver) {
+ COLLECT_REPORT("explicit/telemetry/IOObserver",
+ sTelemetryIOObserver->SizeOfIncludingThis(aMallocSizeOf),
+ "Memory used by the Telemetry IO Observer");
+ }
+
+ COLLECT_REPORT("explicit/telemetry/LateWritesStacks",
+ mLateWritesStacks.SizeOfExcludingThis(),
+ "Memory used by the Telemetry LateWrites Stack capturer");
+
+ COLLECT_REPORT("explicit/telemetry/Callbacks",
+ mCallbacks.ShallowSizeOfExcludingThis(aMallocSizeOf),
+ "Memory used by the Telemetry Callbacks array (shallow)");
+
+ COLLECT_REPORT(
+ "explicit/telemetry/histogram/data",
+ TelemetryHistogram::GetHistogramSizesOfIncludingThis(aMallocSizeOf),
+ "Memory used by Telemetry Histogram data");
+
+ COLLECT_REPORT("explicit/telemetry/scalar/data",
+ TelemetryScalar::GetScalarSizesOfIncludingThis(aMallocSizeOf),
+ "Memory used by Telemetry Scalar data");
+
+ COLLECT_REPORT("explicit/telemetry/event/data",
+ TelemetryEvent::SizeOfIncludingThis(aMallocSizeOf),
+ "Memory used by Telemetry Event data");
+
+#undef COLLECT_REPORT
+
+ return NS_OK;
+}
+
+void InitHistogramRecordingEnabled() {
+ TelemetryHistogram::InitHistogramRecordingEnabled();
+}
+
+using PathChar = filesystem::Path::value_type;
+using PathCharPtr = const PathChar*;
+
+static uint32_t ReadLastShutdownDuration(PathCharPtr filename) {
+ RefPtr<nsLocalFile> file =
+ new nsLocalFile(nsTDependentString<PathChar>(filename));
+ FILE* f;
+ if (NS_FAILED(file->OpenANSIFileDesc("r", &f)) || !f) {
+ return 0;
+ }
+
+ int shutdownTime;
+ int r = fscanf(f, "%d\n", &shutdownTime);
+ fclose(f);
+ if (r != 1) {
+ return 0;
+ }
+
+ return shutdownTime;
+}
+
+const int32_t kMaxFailedProfileLockFileSize = 10;
+
+bool GetFailedLockCount(nsIInputStream* inStream, uint32_t aCount,
+ unsigned int& result) {
+ nsAutoCString bufStr;
+ nsresult rv;
+ rv = NS_ReadInputStreamToString(inStream, bufStr, aCount);
+ NS_ENSURE_SUCCESS(rv, false);
+ result = bufStr.ToInteger(&rv);
+ return NS_SUCCEEDED(rv) && result > 0;
+}
+
+nsresult GetFailedProfileLockFile(nsIFile** aFile, nsIFile* aProfileDir) {
+ NS_ENSURE_ARG_POINTER(aProfileDir);
+
+ nsresult rv = aProfileDir->Clone(aFile);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ (*aFile)->AppendNative("Telemetry.FailedProfileLocks.txt"_ns);
+ return NS_OK;
+}
+
+class nsFetchTelemetryData : public Runnable {
+ public:
+ nsFetchTelemetryData(PathCharPtr aShutdownTimeFilename,
+ nsIFile* aFailedProfileLockFile, nsIFile* aProfileDir)
+ : mozilla::Runnable("nsFetchTelemetryData"),
+ mShutdownTimeFilename(aShutdownTimeFilename),
+ mFailedProfileLockFile(aFailedProfileLockFile),
+ mProfileDir(aProfileDir) {}
+
+ private:
+ PathCharPtr mShutdownTimeFilename;
+ nsCOMPtr<nsIFile> mFailedProfileLockFile;
+ nsCOMPtr<nsIFile> mProfileDir;
+
+ public:
+ void MainThread() {
+ auto lock = TelemetryImpl::sTelemetry.Lock();
+ auto telemetry = lock.ref();
+ telemetry->mCachedTelemetryData = true;
+ for (unsigned int i = 0, n = telemetry->mCallbacks.Count(); i < n; ++i) {
+ telemetry->mCallbacks[i]->Complete();
+ }
+ telemetry->mCallbacks.Clear();
+ }
+
+ NS_IMETHOD Run() override {
+ uint32_t failedLockCount = 0;
+ uint32_t lastShutdownDuration = 0;
+ LoadFailedLockCount(failedLockCount);
+ lastShutdownDuration = ReadLastShutdownDuration(mShutdownTimeFilename);
+ {
+ auto lock = TelemetryImpl::sTelemetry.Lock();
+ auto telemetry = lock.ref();
+ telemetry->mFailedLockCount = failedLockCount;
+ telemetry->mLastShutdownTime = lastShutdownDuration;
+ telemetry->ReadLateWritesStacks(mProfileDir);
+ }
+
+ TelemetryScalar::Set(Telemetry::ScalarID::BROWSER_TIMINGS_LAST_SHUTDOWN,
+ lastShutdownDuration);
+
+ nsCOMPtr<nsIRunnable> e =
+ NewRunnableMethod("nsFetchTelemetryData::MainThread", this,
+ &nsFetchTelemetryData::MainThread);
+ NS_ENSURE_STATE(e);
+ NS_DispatchToMainThread(e);
+ return NS_OK;
+ }
+
+ private:
+ nsresult LoadFailedLockCount(uint32_t& failedLockCount) {
+ failedLockCount = 0;
+ int64_t fileSize = 0;
+ nsresult rv = mFailedProfileLockFile->GetFileSize(&fileSize);
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+ NS_ENSURE_TRUE(fileSize <= kMaxFailedProfileLockFileSize,
+ NS_ERROR_UNEXPECTED);
+ nsCOMPtr<nsIInputStream> inStream;
+ rv = NS_NewLocalFileInputStream(getter_AddRefs(inStream),
+ mFailedProfileLockFile, PR_RDONLY);
+ NS_ENSURE_SUCCESS(rv, rv);
+ NS_ENSURE_TRUE(GetFailedLockCount(inStream, fileSize, failedLockCount),
+ NS_ERROR_UNEXPECTED);
+ inStream->Close();
+
+ mFailedProfileLockFile->Remove(false);
+ return NS_OK;
+ }
+};
+
+static TimeStamp gRecordedShutdownStartTime;
+static bool gAlreadyFreedShutdownTimeFileName = false;
+static PathCharPtr gRecordedShutdownTimeFileName = nullptr;
+
+static PathCharPtr GetShutdownTimeFileName() {
+ if (gAlreadyFreedShutdownTimeFileName) {
+ return nullptr;
+ }
+
+ if (!gRecordedShutdownTimeFileName) {
+ nsCOMPtr<nsIFile> mozFile;
+ NS_GetSpecialDirectory(NS_APP_USER_PROFILE_50_DIR, getter_AddRefs(mozFile));
+ if (!mozFile) return nullptr;
+
+ mozFile->AppendNative("Telemetry.ShutdownTime.txt"_ns);
+
+ gRecordedShutdownTimeFileName = NS_xstrdup(mozFile->NativePath().get());
+ }
+
+ return gRecordedShutdownTimeFileName;
+}
+
+NS_IMETHODIMP
+TelemetryImpl::GetLastShutdownDuration(uint32_t* aResult) {
+ // The user must call AsyncFetchTelemetryData first. We return zero instead of
+ // reporting a failure so that the rest of telemetry can uniformly handle
+ // the read not being available yet.
+ if (!mCachedTelemetryData) {
+ *aResult = 0;
+ return NS_OK;
+ }
+
+ *aResult = mLastShutdownTime;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+TelemetryImpl::GetFailedProfileLockCount(uint32_t* aResult) {
+ // The user must call AsyncFetchTelemetryData first. We return zero instead of
+ // reporting a failure so that the rest of telemetry can uniformly handle
+ // the read not being available yet.
+ if (!mCachedTelemetryData) {
+ *aResult = 0;
+ return NS_OK;
+ }
+
+ *aResult = mFailedLockCount;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+TelemetryImpl::AsyncFetchTelemetryData(
+ nsIFetchTelemetryDataCallback* aCallback) {
+ // We have finished reading the data already, just call the callback.
+ if (mCachedTelemetryData) {
+ aCallback->Complete();
+ return NS_OK;
+ }
+
+ // We already have a read request running, just remember the callback.
+ if (mCallbacks.Count() != 0) {
+ mCallbacks.AppendObject(aCallback);
+ return NS_OK;
+ }
+
+ // We make this check so that GetShutdownTimeFileName() doesn't get
+ // called; calling that function without telemetry enabled violates
+ // assumptions that the write-the-shutdown-timestamp machinery makes.
+ if (!Telemetry::CanRecordExtended()) {
+ mCachedTelemetryData = true;
+ aCallback->Complete();
+ return NS_OK;
+ }
+
+ // Send the read to a background thread provided by the stream transport
+ // service to avoid a read in the main thread.
+ nsCOMPtr<nsIEventTarget> targetThread =
+ do_GetService(NS_STREAMTRANSPORTSERVICE_CONTRACTID);
+ if (!targetThread) {
+ mCachedTelemetryData = true;
+ aCallback->Complete();
+ return NS_OK;
+ }
+
+ // We have to get the filename from the main thread.
+ PathCharPtr shutdownTimeFilename = GetShutdownTimeFileName();
+ if (!shutdownTimeFilename) {
+ mCachedTelemetryData = true;
+ aCallback->Complete();
+ return NS_OK;
+ }
+
+ nsCOMPtr<nsIFile> profileDir;
+ nsresult rv = NS_GetSpecialDirectory(NS_APP_USER_PROFILE_50_DIR,
+ getter_AddRefs(profileDir));
+ if (NS_FAILED(rv)) {
+ mCachedTelemetryData = true;
+ aCallback->Complete();
+ return NS_OK;
+ }
+
+ nsCOMPtr<nsIFile> failedProfileLockFile;
+ rv = GetFailedProfileLockFile(getter_AddRefs(failedProfileLockFile),
+ profileDir);
+ if (NS_FAILED(rv)) {
+ mCachedTelemetryData = true;
+ aCallback->Complete();
+ return NS_OK;
+ }
+
+ mCallbacks.AppendObject(aCallback);
+
+ nsCOMPtr<nsIRunnable> event = new nsFetchTelemetryData(
+ shutdownTimeFilename, failedProfileLockFile, profileDir);
+
+ targetThread->Dispatch(event, NS_DISPATCH_NORMAL);
+ return NS_OK;
+}
+
+TelemetryImpl::TelemetryImpl()
+ : mHashMutex("Telemetry::mHashMutex"),
+ mCanRecordBase(false),
+ mCanRecordExtended(false),
+ mCachedTelemetryData(false),
+ mLastShutdownTime(0),
+ mFailedLockCount(0) {
+ // We expect TelemetryHistogram::InitializeGlobalState() to have been
+ // called before we get to this point.
+ MOZ_ASSERT(TelemetryHistogram::GlobalStateHasBeenInitialized());
+}
+
+TelemetryImpl::~TelemetryImpl() {
+ UnregisterWeakMemoryReporter(this);
+
+ // This is still racey as access to these collections is guarded using
+ // sTelemetry. We will fix this in bug 1367344.
+ MutexAutoLock hashLock(mHashMutex);
+}
+
+void TelemetryImpl::InitMemoryReporter() { RegisterWeakMemoryReporter(this); }
+
+bool TelemetryImpl::ReflectSQL(const SlowSQLEntryType* entry, const Stat* stat,
+ JSContext* cx, JS::Handle<JSObject*> obj) {
+ if (stat->hitCount == 0) return true;
+
+ const nsACString& sql = entry->GetKey();
+
+ JS::Rooted<JSObject*> arrayObj(cx, JS::NewArrayObject(cx, 0));
+ if (!arrayObj) {
+ return false;
+ }
+ return (
+ JS_DefineElement(cx, arrayObj, 0, stat->hitCount, JSPROP_ENUMERATE) &&
+ JS_DefineElement(cx, arrayObj, 1, stat->totalTime, JSPROP_ENUMERATE) &&
+ JS_DefineProperty(cx, obj, sql.BeginReading(), arrayObj,
+ JSPROP_ENUMERATE));
+}
+
+bool TelemetryImpl::ReflectMainThreadSQL(SlowSQLEntryType* entry, JSContext* cx,
+ JS::Handle<JSObject*> obj) {
+ return ReflectSQL(entry, &entry->GetModifiableData()->mainThread, cx, obj);
+}
+
+bool TelemetryImpl::ReflectOtherThreadsSQL(SlowSQLEntryType* entry,
+ JSContext* cx,
+ JS::Handle<JSObject*> obj) {
+ return ReflectSQL(entry, &entry->GetModifiableData()->otherThreads, cx, obj);
+}
+
+bool TelemetryImpl::AddSQLInfo(JSContext* cx, JS::Handle<JSObject*> rootObj,
+ bool mainThread, bool privateSQL) {
+ JS::Rooted<JSObject*> statsObj(cx, JS_NewPlainObject(cx));
+ if (!statsObj) return false;
+
+ AutoHashtable<SlowSQLEntryType>& sqlMap =
+ (privateSQL ? mPrivateSQL : mSanitizedSQL);
+ AutoHashtable<SlowSQLEntryType>::ReflectEntryFunc reflectFunction =
+ (mainThread ? ReflectMainThreadSQL : ReflectOtherThreadsSQL);
+ if (!sqlMap.ReflectIntoJS(reflectFunction, cx, statsObj)) {
+ return false;
+ }
+
+ return JS_DefineProperty(cx, rootObj,
+ mainThread ? "mainThread" : "otherThreads", statsObj,
+ JSPROP_ENUMERATE);
+}
+
+NS_IMETHODIMP
+TelemetryImpl::SetHistogramRecordingEnabled(const nsACString& id,
+ bool aEnabled) {
+ return TelemetryHistogram::SetHistogramRecordingEnabled(id, aEnabled);
+}
+
+NS_IMETHODIMP
+TelemetryImpl::GetSnapshotForHistograms(const nsACString& aStoreName,
+ bool aClearStore, bool aFilterTest,
+ JSContext* aCx,
+ JS::MutableHandle<JS::Value> aResult) {
+ constexpr auto defaultStore = "main"_ns;
+ unsigned int dataset = mCanRecordExtended
+ ? nsITelemetry::DATASET_PRERELEASE_CHANNELS
+ : nsITelemetry::DATASET_ALL_CHANNELS;
+ return TelemetryHistogram::CreateHistogramSnapshots(
+ aCx, aResult, aStoreName.IsVoid() ? defaultStore : aStoreName, dataset,
+ aClearStore, aFilterTest);
+}
+
+NS_IMETHODIMP
+TelemetryImpl::GetSnapshotForKeyedHistograms(
+ const nsACString& aStoreName, bool aClearStore, bool aFilterTest,
+ JSContext* aCx, JS::MutableHandle<JS::Value> aResult) {
+ constexpr auto defaultStore = "main"_ns;
+ unsigned int dataset = mCanRecordExtended
+ ? nsITelemetry::DATASET_PRERELEASE_CHANNELS
+ : nsITelemetry::DATASET_ALL_CHANNELS;
+ return TelemetryHistogram::GetKeyedHistogramSnapshots(
+ aCx, aResult, aStoreName.IsVoid() ? defaultStore : aStoreName, dataset,
+ aClearStore, aFilterTest);
+}
+
+NS_IMETHODIMP
+TelemetryImpl::GetCategoricalLabels(JSContext* aCx,
+ JS::MutableHandle<JS::Value> aResult) {
+ return TelemetryHistogram::GetCategoricalHistogramLabels(aCx, aResult);
+}
+
+NS_IMETHODIMP
+TelemetryImpl::GetSnapshotForScalars(const nsACString& aStoreName,
+ bool aClearStore, bool aFilterTest,
+ JSContext* aCx,
+ JS::MutableHandle<JS::Value> aResult) {
+ constexpr auto defaultStore = "main"_ns;
+ unsigned int dataset = mCanRecordExtended
+ ? nsITelemetry::DATASET_PRERELEASE_CHANNELS
+ : nsITelemetry::DATASET_ALL_CHANNELS;
+ return TelemetryScalar::CreateSnapshots(
+ dataset, aClearStore, aCx, 1, aResult, aFilterTest,
+ aStoreName.IsVoid() ? defaultStore : aStoreName);
+}
+
+NS_IMETHODIMP
+TelemetryImpl::GetSnapshotForKeyedScalars(
+ const nsACString& aStoreName, bool aClearStore, bool aFilterTest,
+ JSContext* aCx, JS::MutableHandle<JS::Value> aResult) {
+ constexpr auto defaultStore = "main"_ns;
+ unsigned int dataset = mCanRecordExtended
+ ? nsITelemetry::DATASET_PRERELEASE_CHANNELS
+ : nsITelemetry::DATASET_ALL_CHANNELS;
+ return TelemetryScalar::CreateKeyedSnapshots(
+ dataset, aClearStore, aCx, 1, aResult, aFilterTest,
+ aStoreName.IsVoid() ? defaultStore : aStoreName);
+}
+
+bool TelemetryImpl::GetSQLStats(JSContext* cx, JS::MutableHandle<JS::Value> ret,
+ bool includePrivateSql) {
+ JS::Rooted<JSObject*> root_obj(cx, JS_NewPlainObject(cx));
+ if (!root_obj) return false;
+ ret.setObject(*root_obj);
+
+ MutexAutoLock hashMutex(mHashMutex);
+ // Add info about slow SQL queries on the main thread
+ if (!AddSQLInfo(cx, root_obj, true, includePrivateSql)) return false;
+ // Add info about slow SQL queries on other threads
+ if (!AddSQLInfo(cx, root_obj, false, includePrivateSql)) return false;
+
+ return true;
+}
+
+NS_IMETHODIMP
+TelemetryImpl::GetSlowSQL(JSContext* cx, JS::MutableHandle<JS::Value> ret) {
+ if (GetSQLStats(cx, ret, false)) return NS_OK;
+ return NS_ERROR_FAILURE;
+}
+
+NS_IMETHODIMP
+TelemetryImpl::GetDebugSlowSQL(JSContext* cx,
+ JS::MutableHandle<JS::Value> ret) {
+ bool revealPrivateSql =
+ Preferences::GetBool("toolkit.telemetry.debugSlowSql", false);
+ if (GetSQLStats(cx, ret, revealPrivateSql)) return NS_OK;
+ return NS_ERROR_FAILURE;
+}
+
+NS_IMETHODIMP
+TelemetryImpl::GetUntrustedModuleLoadEvents(uint32_t aFlags, JSContext* cx,
+ Promise** aPromise) {
+#if defined(XP_WIN)
+ return Telemetry::GetUntrustedModuleLoadEvents(aFlags, cx, aPromise);
+#else
+ return NS_ERROR_NOT_IMPLEMENTED;
+#endif
+}
+
+NS_IMETHODIMP
+TelemetryImpl::GetAreUntrustedModuleLoadEventsReady(bool* ret) {
+#if defined(XP_WIN)
+ *ret = DllServices::Get()->IsReadyForBackgroundProcessing();
+ return NS_OK;
+#else
+ return NS_ERROR_NOT_IMPLEMENTED;
+#endif
+}
+
+#if defined(MOZ_GECKO_PROFILER)
+class GetLoadedModulesResultRunnable final : public Runnable {
+ nsMainThreadPtrHandle<Promise> mPromise;
+ SharedLibraryInfo mRawModules;
+ nsCOMPtr<nsIThread> mWorkerThread;
+# if defined(XP_WIN)
+ nsTHashMap<nsStringHashKey, nsString> mCertSubjects;
+# endif // defined(XP_WIN)
+
+ public:
+ GetLoadedModulesResultRunnable(const nsMainThreadPtrHandle<Promise>& aPromise,
+ const SharedLibraryInfo& rawModules)
+ : mozilla::Runnable("GetLoadedModulesResultRunnable"),
+ mPromise(aPromise),
+ mRawModules(rawModules),
+ mWorkerThread(do_GetCurrentThread()) {
+ MOZ_ASSERT(!NS_IsMainThread());
+# if defined(XP_WIN)
+ ObtainCertSubjects();
+# endif // defined(XP_WIN)
+ }
+
+ NS_IMETHOD
+ Run() override {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ mWorkerThread->Shutdown();
+
+ AutoJSAPI jsapi;
+ if (NS_WARN_IF(!jsapi.Init(mPromise->GetGlobalObject()))) {
+ mPromise->MaybeReject(NS_ERROR_FAILURE);
+ return NS_OK;
+ }
+
+ JSContext* cx = jsapi.cx();
+
+ JS::Rooted<JSObject*> moduleArray(cx, JS::NewArrayObject(cx, 0));
+ if (!moduleArray) {
+ mPromise->MaybeReject(NS_ERROR_FAILURE);
+ return NS_OK;
+ }
+
+ for (unsigned int i = 0, n = mRawModules.GetSize(); i != n; i++) {
+ const SharedLibrary& info = mRawModules.GetEntry(i);
+
+ JS::Rooted<JSObject*> moduleObj(cx, JS_NewPlainObject(cx));
+ if (!moduleObj) {
+ mPromise->MaybeReject(NS_ERROR_FAILURE);
+ return NS_OK;
+ }
+
+ // Module name.
+ JS::Rooted<JSString*> moduleName(
+ cx, JS_NewUCStringCopyZ(cx, info.GetModuleName().get()));
+ if (!moduleName || !JS_DefineProperty(cx, moduleObj, "name", moduleName,
+ JSPROP_ENUMERATE)) {
+ mPromise->MaybeReject(NS_ERROR_FAILURE);
+ return NS_OK;
+ }
+
+ // Module debug name.
+ JS::Rooted<JS::Value> moduleDebugName(cx);
+
+ if (!info.GetDebugName().IsEmpty()) {
+ JS::Rooted<JSString*> str_moduleDebugName(
+ cx, JS_NewUCStringCopyZ(cx, info.GetDebugName().get()));
+ if (!str_moduleDebugName) {
+ mPromise->MaybeReject(NS_ERROR_FAILURE);
+ return NS_OK;
+ }
+ moduleDebugName.setString(str_moduleDebugName);
+ } else {
+ moduleDebugName.setNull();
+ }
+
+ if (!JS_DefineProperty(cx, moduleObj, "debugName", moduleDebugName,
+ JSPROP_ENUMERATE)) {
+ mPromise->MaybeReject(NS_ERROR_FAILURE);
+ return NS_OK;
+ }
+
+ // Module Breakpad identifier.
+ JS::Rooted<JS::Value> id(cx);
+
+ if (!info.GetBreakpadId().IsEmpty()) {
+ JS::Rooted<JSString*> str_id(
+ cx, JS_NewStringCopyZ(cx, info.GetBreakpadId().get()));
+ if (!str_id) {
+ mPromise->MaybeReject(NS_ERROR_FAILURE);
+ return NS_OK;
+ }
+ id.setString(str_id);
+ } else {
+ id.setNull();
+ }
+
+ if (!JS_DefineProperty(cx, moduleObj, "debugID", id, JSPROP_ENUMERATE)) {
+ mPromise->MaybeReject(NS_ERROR_FAILURE);
+ return NS_OK;
+ }
+
+ // Module version.
+ JS::Rooted<JS::Value> version(cx);
+
+ if (!info.GetVersion().IsEmpty()) {
+ JS::Rooted<JSString*> v(
+ cx, JS_NewStringCopyZ(cx, info.GetVersion().BeginReading()));
+ if (!v) {
+ mPromise->MaybeReject(NS_ERROR_FAILURE);
+ return NS_OK;
+ }
+ version.setString(v);
+ } else {
+ version.setNull();
+ }
+
+ if (!JS_DefineProperty(cx, moduleObj, "version", version,
+ JSPROP_ENUMERATE)) {
+ mPromise->MaybeReject(NS_ERROR_FAILURE);
+ return NS_OK;
+ }
+
+# if defined(XP_WIN)
+ // Cert Subject.
+ if (auto subject = mCertSubjects.Lookup(info.GetModulePath())) {
+ JS::Rooted<JSString*> jsOrg(cx, ToJSString(cx, *subject));
+ if (!jsOrg) {
+ mPromise->MaybeReject(NS_ERROR_FAILURE);
+ return NS_OK;
+ }
+
+ JS::Rooted<JS::Value> certSubject(cx);
+ certSubject.setString(jsOrg);
+
+ if (!JS_DefineProperty(cx, moduleObj, "certSubject", certSubject,
+ JSPROP_ENUMERATE)) {
+ mPromise->MaybeReject(NS_ERROR_FAILURE);
+ return NS_OK;
+ }
+ }
+# endif // defined(XP_WIN)
+
+ if (!JS_DefineElement(cx, moduleArray, i, moduleObj, JSPROP_ENUMERATE)) {
+ mPromise->MaybeReject(NS_ERROR_FAILURE);
+ return NS_OK;
+ }
+ }
+
+ mPromise->MaybeResolve(moduleArray);
+ return NS_OK;
+ }
+
+ private:
+# if defined(XP_WIN)
+ void ObtainCertSubjects() {
+ MOZ_ASSERT(!NS_IsMainThread());
+
+ // NB: Currently we cannot lower this down to the profiler layer due to
+ // differing startup dependencies between the profiler and DllServices.
+ RefPtr<DllServices> dllSvc(DllServices::Get());
+
+ for (unsigned int i = 0, n = mRawModules.GetSize(); i != n; i++) {
+ const SharedLibrary& info = mRawModules.GetEntry(i);
+
+ auto orgName = dllSvc->GetBinaryOrgName(info.GetModulePath().get());
+ if (orgName) {
+ mCertSubjects.InsertOrUpdate(info.GetModulePath(),
+ nsDependentString(orgName.get()));
+ }
+ }
+ }
+# endif // defined(XP_WIN)
+};
+
+class GetLoadedModulesRunnable final : public Runnable {
+ nsMainThreadPtrHandle<Promise> mPromise;
+
+ public:
+ explicit GetLoadedModulesRunnable(
+ const nsMainThreadPtrHandle<Promise>& aPromise)
+ : mozilla::Runnable("GetLoadedModulesRunnable"), mPromise(aPromise) {}
+
+ NS_IMETHOD
+ Run() override {
+ nsCOMPtr<nsIRunnable> resultRunnable = new GetLoadedModulesResultRunnable(
+ mPromise, SharedLibraryInfo::GetInfoForSelf());
+ return NS_DispatchToMainThread(resultRunnable);
+ }
+};
+#endif // MOZ_GECKO_PROFILER
+
+NS_IMETHODIMP
+TelemetryImpl::GetLoadedModules(JSContext* cx, Promise** aPromise) {
+#if defined(MOZ_GECKO_PROFILER)
+ nsIGlobalObject* global = xpc::CurrentNativeGlobal(cx);
+ if (NS_WARN_IF(!global)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ ErrorResult result;
+ RefPtr<Promise> promise = Promise::Create(global, result);
+ if (NS_WARN_IF(result.Failed())) {
+ return result.StealNSResult();
+ }
+
+ nsCOMPtr<nsIThread> getModulesThread;
+ nsresult rv =
+ NS_NewNamedThread("TelemetryModule", getter_AddRefs(getModulesThread));
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ promise->MaybeReject(NS_ERROR_FAILURE);
+ return NS_OK;
+ }
+
+ nsMainThreadPtrHandle<Promise> mainThreadPromise(
+ new nsMainThreadPtrHolder<Promise>(
+ "TelemetryImpl::GetLoadedModules::Promise", promise));
+ nsCOMPtr<nsIRunnable> runnable =
+ new GetLoadedModulesRunnable(mainThreadPromise);
+ promise.forget(aPromise);
+
+ return getModulesThread->Dispatch(runnable, nsIEventTarget::DISPATCH_NORMAL);
+#else // MOZ_GECKO_PROFILER
+ return NS_ERROR_NOT_IMPLEMENTED;
+#endif // MOZ_GECKO_PROFILER
+}
+
+static bool IsValidBreakpadId(const std::string& breakpadId) {
+ if (breakpadId.size() < 33) {
+ return false;
+ }
+ for (char c : breakpadId) {
+ if ((c < '0' || c > '9') && (c < 'A' || c > 'F')) {
+ return false;
+ }
+ }
+ return true;
+}
+
+// Read a stack from the given file name. In case of any error, aStack is
+// unchanged.
+static void ReadStack(PathCharPtr aFileName,
+ Telemetry::ProcessedStack& aStack) {
+ IFStream file(aFileName);
+
+ size_t numModules;
+ file >> numModules;
+ if (file.fail()) {
+ return;
+ }
+
+ char newline = file.get();
+ if (file.fail() || newline != '\n') {
+ return;
+ }
+
+ Telemetry::ProcessedStack stack;
+ for (size_t i = 0; i < numModules; ++i) {
+ std::string breakpadId;
+ file >> breakpadId;
+ if (file.fail() || !IsValidBreakpadId(breakpadId)) {
+ return;
+ }
+
+ char space = file.get();
+ if (file.fail() || space != ' ') {
+ return;
+ }
+
+ std::string moduleName;
+ getline(file, moduleName);
+ if (file.fail() || moduleName[0] == ' ') {
+ return;
+ }
+
+ Telemetry::ProcessedStack::Module module = {
+ NS_ConvertUTF8toUTF16(moduleName.c_str()),
+ nsCString(breakpadId.c_str(), breakpadId.size()),
+ };
+ stack.AddModule(module);
+ }
+
+ size_t numFrames;
+ file >> numFrames;
+ if (file.fail()) {
+ return;
+ }
+
+ newline = file.get();
+ if (file.fail() || newline != '\n') {
+ return;
+ }
+
+ for (size_t i = 0; i < numFrames; ++i) {
+ uint16_t index;
+ file >> index;
+ uintptr_t offset;
+ file >> std::hex >> offset >> std::dec;
+ if (file.fail()) {
+ return;
+ }
+
+ Telemetry::ProcessedStack::Frame frame = {offset, index};
+ stack.AddFrame(frame);
+ }
+
+ aStack = stack;
+}
+
+void TelemetryImpl::ReadLateWritesStacks(nsIFile* aProfileDir) {
+ nsCOMPtr<nsIDirectoryEnumerator> files;
+ if (NS_FAILED(aProfileDir->GetDirectoryEntries(getter_AddRefs(files)))) {
+ return;
+ }
+
+ constexpr auto prefix = u"Telemetry.LateWriteFinal-"_ns;
+ nsCOMPtr<nsIFile> file;
+ while (NS_SUCCEEDED(files->GetNextFile(getter_AddRefs(file))) && file) {
+ nsAutoString leafName;
+ if (NS_FAILED(file->GetLeafName(leafName)) ||
+ !StringBeginsWith(leafName, prefix)) {
+ continue;
+ }
+
+ Telemetry::ProcessedStack stack;
+ ReadStack(file->NativePath().get(), stack);
+ if (stack.GetStackSize() != 0) {
+ mLateWritesStacks.AddStack(stack);
+ }
+ // Delete the file so that we don't report it again on the next run.
+ file->Remove(false);
+ }
+}
+
+NS_IMETHODIMP
+TelemetryImpl::GetLateWrites(JSContext* cx, JS::MutableHandle<JS::Value> ret) {
+ // The user must call AsyncReadTelemetryData first. We return an empty list
+ // instead of reporting a failure so that the rest of telemetry can uniformly
+ // handle the read not being available yet.
+
+ // FIXME: we allocate the js object again and again in the getter. We should
+ // figure out a way to cache it. In order to do that we have to call
+ // JS_AddNamedObjectRoot. A natural place to do so is in the TelemetryImpl
+ // constructor, but it is not clear how to get a JSContext in there.
+ // Another option would be to call it in here when we first call
+ // CreateJSStackObject, but we would still need to figure out where to call
+ // JS_RemoveObjectRoot. Would it be ok to never call JS_RemoveObjectRoot
+ // and just set the pointer to nullptr is the telemetry destructor?
+
+ JSObject* report;
+ if (!mCachedTelemetryData) {
+ CombinedStacks empty;
+ report = CreateJSStackObject(cx, empty);
+ } else {
+ report = CreateJSStackObject(cx, mLateWritesStacks);
+ }
+
+ if (report == nullptr) {
+ return NS_ERROR_FAILURE;
+ }
+
+ ret.setObject(*report);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+TelemetryImpl::GetHistogramById(const nsACString& name, JSContext* cx,
+ JS::MutableHandle<JS::Value> ret) {
+ return TelemetryHistogram::GetHistogramById(name, cx, ret);
+}
+
+NS_IMETHODIMP
+TelemetryImpl::GetKeyedHistogramById(const nsACString& name, JSContext* cx,
+ JS::MutableHandle<JS::Value> ret) {
+ return TelemetryHistogram::GetKeyedHistogramById(name, cx, ret);
+}
+
+/**
+ * Indicates if Telemetry can record base data (FHR data). This is true if the
+ * FHR data reporting service or self-support are enabled.
+ *
+ * In the unlikely event that adding a new base probe is needed, please check
+ * the data collection wiki at https://wiki.mozilla.org/Firefox/Data_Collection
+ * and talk to the Telemetry team.
+ */
+NS_IMETHODIMP
+TelemetryImpl::GetCanRecordBase(bool* ret) {
+ *ret = mCanRecordBase;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+TelemetryImpl::SetCanRecordBase(bool canRecord) {
+#ifndef FUZZING
+ if (canRecord != mCanRecordBase) {
+ TelemetryHistogram::SetCanRecordBase(canRecord);
+ TelemetryScalar::SetCanRecordBase(canRecord);
+ TelemetryEvent::SetCanRecordBase(canRecord);
+ mCanRecordBase = canRecord;
+ }
+#endif
+ return NS_OK;
+}
+
+/**
+ * Indicates if Telemetry is allowed to record extended data. Returns false if
+ * the user hasn't opted into "extended Telemetry" on the Release channel, when
+ * the user has explicitly opted out of Telemetry on Nightly/Aurora/Beta or if
+ * manually set to false during tests. If the returned value is false, gathering
+ * of extended telemetry statistics is disabled.
+ */
+NS_IMETHODIMP
+TelemetryImpl::GetCanRecordExtended(bool* ret) {
+ *ret = mCanRecordExtended;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+TelemetryImpl::SetCanRecordExtended(bool canRecord) {
+#ifndef FUZZING
+ if (canRecord != mCanRecordExtended) {
+ TelemetryHistogram::SetCanRecordExtended(canRecord);
+ TelemetryScalar::SetCanRecordExtended(canRecord);
+ TelemetryEvent::SetCanRecordExtended(canRecord);
+ mCanRecordExtended = canRecord;
+ }
+#endif
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+TelemetryImpl::GetCanRecordReleaseData(bool* ret) {
+ *ret = mCanRecordBase;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+TelemetryImpl::GetCanRecordPrereleaseData(bool* ret) {
+ *ret = mCanRecordExtended;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+TelemetryImpl::GetIsOfficialTelemetry(bool* ret) {
+#if defined(MOZILLA_OFFICIAL) && defined(MOZ_TELEMETRY_REPORTING) && \
+ !defined(DEBUG)
+ *ret = true;
+#else
+ *ret = false;
+#endif
+ return NS_OK;
+}
+
+already_AddRefed<nsITelemetry> TelemetryImpl::CreateTelemetryInstance() {
+ {
+ auto lock = sTelemetry.Lock();
+ MOZ_ASSERT(
+ *lock == nullptr,
+ "CreateTelemetryInstance may only be called once, via GetService()");
+ }
+
+ bool useTelemetry = false;
+#ifndef FUZZING
+ if (XRE_IsParentProcess() || XRE_IsContentProcess() || XRE_IsGPUProcess() ||
+ XRE_IsRDDProcess() || XRE_IsSocketProcess() || XRE_IsUtilityProcess()) {
+ useTelemetry = true;
+ }
+#endif
+#ifdef MOZ_BACKGROUNDTASKS
+ if (BackgroundTasks::IsBackgroundTaskMode()) {
+ // Background tasks collect per-task metrics with Glean.
+ useTelemetry = false;
+ }
+#endif
+
+ // First, initialize the TelemetryHistogram and TelemetryScalar global states.
+ TelemetryHistogram::InitializeGlobalState(useTelemetry, useTelemetry);
+ TelemetryScalar::InitializeGlobalState(useTelemetry, useTelemetry);
+
+ // Only record events from the parent process.
+ TelemetryEvent::InitializeGlobalState(XRE_IsParentProcess(),
+ XRE_IsParentProcess());
+
+ // Currently, only UserInteractions from the parent process are recorded.
+ TelemetryUserInteraction::InitializeGlobalState(useTelemetry, useTelemetry);
+
+ // Now, create and initialize the Telemetry global state.
+ TelemetryImpl* telemetry = new TelemetryImpl();
+ {
+ auto lock = sTelemetry.Lock();
+ *lock = telemetry;
+ // AddRef for the local reference before releasing the lock.
+ NS_ADDREF(telemetry);
+ }
+
+ // AddRef for the caller
+ nsCOMPtr<nsITelemetry> ret = telemetry;
+
+ telemetry->mCanRecordBase = useTelemetry;
+ telemetry->mCanRecordExtended = useTelemetry;
+
+ telemetry->InitMemoryReporter();
+ InitHistogramRecordingEnabled(); // requires sTelemetry to exist
+
+ return ret.forget();
+}
+
+void TelemetryImpl::ShutdownTelemetry() {
+ // No point in collecting IO beyond this point
+ ClearIOReporting();
+ {
+ auto lock = sTelemetry.Lock();
+ NS_IF_RELEASE(lock.ref());
+ }
+
+ // Lastly, de-initialise the TelemetryHistogram and TelemetryScalar global
+ // states, so as to release any heap storage that would otherwise be kept
+ // alive by it.
+ TelemetryHistogram::DeInitializeGlobalState();
+ TelemetryScalar::DeInitializeGlobalState();
+ TelemetryEvent::DeInitializeGlobalState();
+
+ TelemetryUserInteraction::DeInitializeGlobalState();
+ TelemetryIPCAccumulator::DeInitializeGlobalState();
+}
+
+void TelemetryImpl::StoreSlowSQL(const nsACString& sql, uint32_t delay,
+ SanitizedState state) {
+ auto lock = sTelemetry.Lock();
+ auto telemetry = lock.ref();
+ AutoHashtable<SlowSQLEntryType>* slowSQLMap = nullptr;
+ if (state == Sanitized)
+ slowSQLMap = &(telemetry->mSanitizedSQL);
+ else
+ slowSQLMap = &(telemetry->mPrivateSQL);
+
+ MutexAutoLock hashMutex(telemetry->mHashMutex);
+
+ SlowSQLEntryType* entry = slowSQLMap->GetEntry(sql);
+ if (!entry) {
+ entry = slowSQLMap->PutEntry(sql);
+ if (MOZ_UNLIKELY(!entry)) return;
+ entry->GetModifiableData()->mainThread.hitCount = 0;
+ entry->GetModifiableData()->mainThread.totalTime = 0;
+ entry->GetModifiableData()->otherThreads.hitCount = 0;
+ entry->GetModifiableData()->otherThreads.totalTime = 0;
+ }
+
+ if (NS_IsMainThread()) {
+ entry->GetModifiableData()->mainThread.hitCount++;
+ entry->GetModifiableData()->mainThread.totalTime += delay;
+ } else {
+ entry->GetModifiableData()->otherThreads.hitCount++;
+ entry->GetModifiableData()->otherThreads.totalTime += delay;
+ }
+}
+
+/**
+ * This method replaces string literals in SQL strings with the word :private
+ *
+ * States used in this state machine:
+ *
+ * NORMAL:
+ * - This is the active state when not iterating over a string literal or
+ * comment
+ *
+ * SINGLE_QUOTE:
+ * - Defined here: http://www.sqlite.org/lang_expr.html
+ * - This state represents iterating over a string literal opened with
+ * a single quote.
+ * - A single quote within the string can be encoded by putting 2 single quotes
+ * in a row, e.g. 'This literal contains an escaped quote '''
+ * - Any double quotes found within a single-quoted literal are ignored
+ * - This state covers BLOB literals, e.g. X'ABC123'
+ * - The string literal and the enclosing quotes will be replaced with
+ * the text :private
+ *
+ * DOUBLE_QUOTE:
+ * - Same rules as the SINGLE_QUOTE state.
+ * - According to http://www.sqlite.org/lang_keywords.html,
+ * SQLite interprets text in double quotes as an identifier unless it's used in
+ * a context where it cannot be resolved to an identifier and a string literal
+ * is allowed. This method removes text in double-quotes for safety.
+ *
+ * DASH_COMMENT:
+ * - http://www.sqlite.org/lang_comment.html
+ * - A dash comment starts with two dashes in a row,
+ * e.g. DROP TABLE foo -- a comment
+ * - Any text following two dashes in a row is interpreted as a comment until
+ * end of input or a newline character
+ * - Any quotes found within the comment are ignored and no replacements made
+ *
+ * C_STYLE_COMMENT:
+ * - http://www.sqlite.org/lang_comment.html
+ * - A C-style comment starts with a forward slash and an asterisk, and ends
+ * with an asterisk and a forward slash
+ * - Any text following comment start is interpreted as a comment up to end of
+ * input or comment end
+ * - Any quotes found within the comment are ignored and no replacements made
+ */
+nsCString TelemetryImpl::SanitizeSQL(const nsACString& sql) {
+ nsCString output;
+ int length = sql.Length();
+
+ typedef enum {
+ NORMAL,
+ SINGLE_QUOTE,
+ DOUBLE_QUOTE,
+ DASH_COMMENT,
+ C_STYLE_COMMENT,
+ } State;
+
+ State state = NORMAL;
+ int fragmentStart = 0;
+ for (int i = 0; i < length; i++) {
+ char character = sql[i];
+ char nextCharacter = (i + 1 < length) ? sql[i + 1] : '\0';
+
+ switch (character) {
+ case '\'':
+ case '"':
+ if (state == NORMAL) {
+ state = (character == '\'') ? SINGLE_QUOTE : DOUBLE_QUOTE;
+ output +=
+ nsDependentCSubstring(sql, fragmentStart, i - fragmentStart);
+ output += ":private";
+ fragmentStart = -1;
+ } else if ((state == SINGLE_QUOTE && character == '\'') ||
+ (state == DOUBLE_QUOTE && character == '"')) {
+ if (nextCharacter == character) {
+ // Two consecutive quotes within a string literal are a single
+ // escaped quote
+ i++;
+ } else {
+ state = NORMAL;
+ fragmentStart = i + 1;
+ }
+ }
+ break;
+ case '-':
+ if (state == NORMAL) {
+ if (nextCharacter == '-') {
+ state = DASH_COMMENT;
+ i++;
+ }
+ }
+ break;
+ case '\n':
+ if (state == DASH_COMMENT) {
+ state = NORMAL;
+ }
+ break;
+ case '/':
+ if (state == NORMAL) {
+ if (nextCharacter == '*') {
+ state = C_STYLE_COMMENT;
+ i++;
+ }
+ }
+ break;
+ case '*':
+ if (state == C_STYLE_COMMENT) {
+ if (nextCharacter == '/') {
+ state = NORMAL;
+ }
+ }
+ break;
+ default:
+ continue;
+ }
+ }
+
+ if ((fragmentStart >= 0) && fragmentStart < length)
+ output += nsDependentCSubstring(sql, fragmentStart, length - fragmentStart);
+
+ return output;
+}
+
+// An allowlist mechanism to prevent Telemetry reporting on Addon & Thunderbird
+// DBs.
+struct TrackedDBEntry {
+ const char* mName;
+ const uint32_t mNameLength;
+
+ // This struct isn't meant to be used beyond the static arrays below.
+ constexpr TrackedDBEntry(const char* aName, uint32_t aNameLength)
+ : mName(aName), mNameLength(aNameLength) {}
+
+ TrackedDBEntry() = delete;
+ TrackedDBEntry(TrackedDBEntry&) = delete;
+};
+
+#define TRACKEDDB_ENTRY(_name) \
+ { _name, (sizeof(_name) - 1) }
+
+// An allowlist of database names. If the database name exactly matches one of
+// these then its SQL statements will always be recorded.
+static constexpr TrackedDBEntry kTrackedDBs[] = {
+ // IndexedDB for about:home, see aboutHome.js
+ TRACKEDDB_ENTRY("818200132aebmoouht.sqlite"),
+ TRACKEDDB_ENTRY("addons.sqlite"),
+ TRACKEDDB_ENTRY("content-prefs.sqlite"),
+ TRACKEDDB_ENTRY("cookies.sqlite"),
+ TRACKEDDB_ENTRY("extensions.sqlite"),
+ TRACKEDDB_ENTRY("favicons.sqlite"),
+ TRACKEDDB_ENTRY("formhistory.sqlite"),
+ TRACKEDDB_ENTRY("index.sqlite"),
+ TRACKEDDB_ENTRY("netpredictions.sqlite"),
+ TRACKEDDB_ENTRY("permissions.sqlite"),
+ TRACKEDDB_ENTRY("places.sqlite"),
+ TRACKEDDB_ENTRY("reading-list.sqlite"),
+ TRACKEDDB_ENTRY("search.sqlite"),
+ TRACKEDDB_ENTRY("urlclassifier3.sqlite"),
+ TRACKEDDB_ENTRY("webappsstore.sqlite")};
+
+// An allowlist of database name prefixes. If the database name begins with
+// one of these prefixes then its SQL statements will always be recorded.
+static const TrackedDBEntry kTrackedDBPrefixes[] = {
+ TRACKEDDB_ENTRY("indexedDB-")};
+
+#undef TRACKEDDB_ENTRY
+
+// Slow SQL statements will be automatically
+// trimmed to kMaxSlowStatementLength characters.
+// This limit doesn't include the ellipsis and DB name,
+// that are appended at the end of the stored statement.
+const uint32_t kMaxSlowStatementLength = 1000;
+
+void TelemetryImpl::RecordSlowStatement(const nsACString& sql,
+ const nsACString& dbName,
+ uint32_t delay) {
+ MOZ_ASSERT(!sql.IsEmpty());
+ MOZ_ASSERT(!dbName.IsEmpty());
+
+ {
+ auto lock = sTelemetry.Lock();
+ if (!lock.ref() || !TelemetryHistogram::CanRecordExtended()) {
+ return;
+ }
+ }
+
+ bool recordStatement = false;
+
+ for (const TrackedDBEntry& nameEntry : kTrackedDBs) {
+ MOZ_ASSERT(nameEntry.mNameLength);
+ const nsDependentCString name(nameEntry.mName, nameEntry.mNameLength);
+ if (dbName == name) {
+ recordStatement = true;
+ break;
+ }
+ }
+
+ if (!recordStatement) {
+ for (const TrackedDBEntry& prefixEntry : kTrackedDBPrefixes) {
+ MOZ_ASSERT(prefixEntry.mNameLength);
+ const nsDependentCString prefix(prefixEntry.mName,
+ prefixEntry.mNameLength);
+ if (StringBeginsWith(dbName, prefix)) {
+ recordStatement = true;
+ break;
+ }
+ }
+ }
+
+ if (recordStatement) {
+ nsAutoCString sanitizedSQL(SanitizeSQL(sql));
+ if (sanitizedSQL.Length() > kMaxSlowStatementLength) {
+ sanitizedSQL.SetLength(kMaxSlowStatementLength);
+ sanitizedSQL += "...";
+ }
+ sanitizedSQL.AppendPrintf(" /* %s */", nsPromiseFlatCString(dbName).get());
+ StoreSlowSQL(sanitizedSQL, delay, Sanitized);
+ } else {
+ // Report aggregate DB-level statistics for addon DBs
+ nsAutoCString aggregate;
+ aggregate.AppendPrintf("Untracked SQL for %s",
+ nsPromiseFlatCString(dbName).get());
+ StoreSlowSQL(aggregate, delay, Sanitized);
+ }
+
+ nsAutoCString fullSQL;
+ fullSQL.AppendPrintf("%s /* %s */", nsPromiseFlatCString(sql).get(),
+ nsPromiseFlatCString(dbName).get());
+ StoreSlowSQL(fullSQL, delay, Unsanitized);
+}
+
+bool TelemetryImpl::CanRecordBase() {
+ auto lock = sTelemetry.Lock();
+ auto telemetry = lock.ref();
+ if (!telemetry) {
+ return false;
+ }
+ bool canRecordBase;
+ nsresult rv = telemetry->GetCanRecordBase(&canRecordBase);
+ return NS_SUCCEEDED(rv) && canRecordBase;
+}
+
+bool TelemetryImpl::CanRecordExtended() {
+ auto lock = sTelemetry.Lock();
+ auto telemetry = lock.ref();
+ if (!telemetry) {
+ return false;
+ }
+ bool canRecordExtended;
+ nsresult rv = telemetry->GetCanRecordExtended(&canRecordExtended);
+ return NS_SUCCEEDED(rv) && canRecordExtended;
+}
+
+bool TelemetryImpl::CanRecordReleaseData() { return CanRecordBase(); }
+
+bool TelemetryImpl::CanRecordPrereleaseData() { return CanRecordExtended(); }
+
+NS_IMPL_ISUPPORTS(TelemetryImpl, nsITelemetry, nsIMemoryReporter)
+
+NS_IMETHODIMP
+TelemetryImpl::GetFileIOReports(JSContext* cx,
+ JS::MutableHandle<JS::Value> ret) {
+ if (sTelemetryIOObserver) {
+ JS::Rooted<JSObject*> obj(cx, JS_NewPlainObject(cx));
+ if (!obj) {
+ return NS_ERROR_FAILURE;
+ }
+
+ if (!sTelemetryIOObserver->ReflectIntoJS(cx, obj)) {
+ return NS_ERROR_FAILURE;
+ }
+ ret.setObject(*obj);
+ return NS_OK;
+ }
+ ret.setNull();
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+TelemetryImpl::MsSinceProcessStart(double* aResult) {
+ return Telemetry::Common::MsSinceProcessStart(aResult);
+}
+
+NS_IMETHODIMP
+TelemetryImpl::MsSinceProcessStartIncludingSuspend(double* aResult) {
+ return Telemetry::Common::MsSinceProcessStartIncludingSuspend(aResult);
+}
+
+NS_IMETHODIMP
+TelemetryImpl::MsSinceProcessStartExcludingSuspend(double* aResult) {
+ return Telemetry::Common::MsSinceProcessStartExcludingSuspend(aResult);
+}
+
+NS_IMETHODIMP
+TelemetryImpl::MsSystemNow(double* aResult) {
+#if defined(XP_UNIX) && !defined(XP_DARWIN)
+ timespec ts;
+ clock_gettime(CLOCK_REALTIME, &ts);
+ *aResult = ts.tv_sec * 1000 + ts.tv_nsec / 1000000;
+#else
+ using namespace std::chrono;
+ milliseconds ms =
+ duration_cast<milliseconds>(system_clock::now().time_since_epoch());
+ *aResult = static_cast<double>(ms.count());
+#endif // XP_UNIX && !XP_DARWIN
+
+ return NS_OK;
+}
+
+// Telemetry Scalars IDL Implementation
+
+NS_IMETHODIMP
+TelemetryImpl::ScalarAdd(const nsACString& aName, JS::Handle<JS::Value> aVal,
+ JSContext* aCx) {
+ return TelemetryScalar::Add(aName, aVal, aCx);
+}
+
+NS_IMETHODIMP
+TelemetryImpl::ScalarSet(const nsACString& aName, JS::Handle<JS::Value> aVal,
+ JSContext* aCx) {
+ return TelemetryScalar::Set(aName, aVal, aCx);
+}
+
+NS_IMETHODIMP
+TelemetryImpl::ScalarSetMaximum(const nsACString& aName,
+ JS::Handle<JS::Value> aVal, JSContext* aCx) {
+ return TelemetryScalar::SetMaximum(aName, aVal, aCx);
+}
+
+NS_IMETHODIMP
+TelemetryImpl::KeyedScalarAdd(const nsACString& aName, const nsAString& aKey,
+ JS::Handle<JS::Value> aVal, JSContext* aCx) {
+ return TelemetryScalar::Add(aName, aKey, aVal, aCx);
+}
+
+NS_IMETHODIMP
+TelemetryImpl::KeyedScalarSet(const nsACString& aName, const nsAString& aKey,
+ JS::Handle<JS::Value> aVal, JSContext* aCx) {
+ return TelemetryScalar::Set(aName, aKey, aVal, aCx);
+}
+
+NS_IMETHODIMP
+TelemetryImpl::KeyedScalarSetMaximum(const nsACString& aName,
+ const nsAString& aKey,
+ JS::Handle<JS::Value> aVal,
+ JSContext* aCx) {
+ return TelemetryScalar::SetMaximum(aName, aKey, aVal, aCx);
+}
+
+NS_IMETHODIMP
+TelemetryImpl::RegisterScalars(const nsACString& aCategoryName,
+ JS::Handle<JS::Value> aScalarData,
+ JSContext* cx) {
+ return TelemetryScalar::RegisterScalars(aCategoryName, aScalarData, false,
+ cx);
+}
+
+NS_IMETHODIMP
+TelemetryImpl::RegisterBuiltinScalars(const nsACString& aCategoryName,
+ JS::Handle<JS::Value> aScalarData,
+ JSContext* cx) {
+ return TelemetryScalar::RegisterScalars(aCategoryName, aScalarData, true, cx);
+}
+
+NS_IMETHODIMP
+TelemetryImpl::ClearScalars() {
+ TelemetryScalar::ClearScalars();
+ return NS_OK;
+}
+
+// Telemetry Event IDL implementation.
+
+NS_IMETHODIMP
+TelemetryImpl::RecordEvent(const nsACString& aCategory,
+ const nsACString& aMethod, const nsACString& aObject,
+ JS::Handle<JS::Value> aValue,
+ JS::Handle<JS::Value> aExtra, JSContext* aCx,
+ uint8_t optional_argc) {
+ return TelemetryEvent::RecordEvent(aCategory, aMethod, aObject, aValue,
+ aExtra, aCx, optional_argc);
+}
+
+NS_IMETHODIMP
+TelemetryImpl::SnapshotEvents(uint32_t aDataset, bool aClear,
+ uint32_t aEventLimit, JSContext* aCx,
+ uint8_t optional_argc,
+ JS::MutableHandle<JS::Value> aResult) {
+ return TelemetryEvent::CreateSnapshots(aDataset, aClear, aEventLimit, aCx,
+ optional_argc, aResult);
+}
+
+NS_IMETHODIMP
+TelemetryImpl::RegisterEvents(const nsACString& aCategory,
+ JS::Handle<JS::Value> aEventData, JSContext* cx) {
+ return TelemetryEvent::RegisterEvents(aCategory, aEventData, false, cx);
+}
+
+NS_IMETHODIMP
+TelemetryImpl::RegisterBuiltinEvents(const nsACString& aCategory,
+ JS::Handle<JS::Value> aEventData,
+ JSContext* cx) {
+ return TelemetryEvent::RegisterEvents(aCategory, aEventData, true, cx);
+}
+
+NS_IMETHODIMP
+TelemetryImpl::ClearEvents() {
+ TelemetryEvent::ClearEvents();
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+TelemetryImpl::SetEventRecordingEnabled(const nsACString& aCategory,
+ bool aEnabled) {
+ TelemetryEvent::SetEventRecordingEnabled(aCategory, aEnabled);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+TelemetryImpl::FlushBatchedChildTelemetry() {
+ TelemetryIPCAccumulator::IPCTimerFired(nullptr, nullptr);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+TelemetryImpl::EarlyInit() {
+ Unused << MemoryTelemetry::Get();
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+TelemetryImpl::DelayedInit() {
+ MemoryTelemetry::Get().DelayedInit();
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+TelemetryImpl::Shutdown() {
+ MemoryTelemetry::Get().Shutdown();
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+TelemetryImpl::GatherMemory(JSContext* aCx, Promise** aResult) {
+ ErrorResult rv;
+ RefPtr<Promise> promise = Promise::Create(xpc::CurrentNativeGlobal(aCx), rv);
+ if (rv.Failed()) {
+ return rv.StealNSResult();
+ }
+
+ MemoryTelemetry::Get().GatherReports(
+ [promise]() { promise->MaybeResolve(JS::UndefinedHandleValue); });
+
+ promise.forget(aResult);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+TelemetryImpl::GetAllStores(JSContext* aCx,
+ JS::MutableHandle<JS::Value> aResult) {
+ StringHashSet stores;
+ nsresult rv;
+
+ rv = TelemetryHistogram::GetAllStores(stores);
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+ rv = TelemetryScalar::GetAllStores(stores);
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+
+ JS::RootedVector<JS::Value> allStores(aCx);
+ if (!allStores.reserve(stores.Count())) {
+ return NS_ERROR_FAILURE;
+ }
+
+ for (const auto& value : stores) {
+ JS::Rooted<JS::Value> store(aCx);
+
+ store.setString(ToJSString(aCx, value));
+ if (!allStores.append(store)) {
+ return NS_ERROR_FAILURE;
+ }
+ }
+
+ JS::Rooted<JSObject*> rarray(aCx, JS::NewArrayObject(aCx, allStores));
+ if (rarray == nullptr) {
+ return NS_ERROR_FAILURE;
+ }
+ aResult.setObject(*rarray);
+
+ return NS_OK;
+}
+
+} // namespace
+
+////////////////////////////////////////////////////////////////////////
+////////////////////////////////////////////////////////////////////////
+//
+// EXTERNALLY VISIBLE FUNCTIONS in no name space
+// These are NOT listed in Telemetry.h
+
+/**
+ * The XRE_TelemetryAdd function is to be used by embedding applications
+ * that can't use mozilla::Telemetry::Accumulate() directly.
+ */
+void XRE_TelemetryAccumulate(int aID, uint32_t aSample) {
+ mozilla::Telemetry::Accumulate((mozilla::Telemetry::HistogramID)aID, aSample);
+}
+
+////////////////////////////////////////////////////////////////////////
+////////////////////////////////////////////////////////////////////////
+//
+// EXTERNALLY VISIBLE FUNCTIONS in mozilla::
+// These are NOT listed in Telemetry.h
+
+namespace mozilla {
+
+void RecordShutdownStartTimeStamp() {
+#ifdef DEBUG
+ // FIXME: this function should only be called once, since it should be called
+ // at the earliest point we *know* we are shutting down. Unfortunately
+ // this assert has been firing. Given that if we are called multiple times
+ // we just keep the last timestamp, the assert is commented for now.
+ static bool recorded = false;
+ // MOZ_ASSERT(!recorded);
+ (void)
+ recorded; // Silence unused-var warnings (remove when assert re-enabled)
+ recorded = true;
+#endif
+
+ if (!Telemetry::CanRecordExtended()) return;
+
+ gRecordedShutdownStartTime = TimeStamp::Now();
+
+ GetShutdownTimeFileName();
+}
+
+void RecordShutdownEndTimeStamp() {
+ if (!gRecordedShutdownTimeFileName || gAlreadyFreedShutdownTimeFileName)
+ return;
+
+ PathString name(gRecordedShutdownTimeFileName);
+ free(const_cast<PathChar*>(gRecordedShutdownTimeFileName));
+ gRecordedShutdownTimeFileName = nullptr;
+ gAlreadyFreedShutdownTimeFileName = true;
+
+ if (gRecordedShutdownStartTime.IsNull()) {
+ // If |CanRecordExtended()| is true before |AsyncFetchTelemetryData| is
+ // called and then disabled before shutdown, |RecordShutdownStartTimeStamp|
+ // will bail out and we will end up with a null |gRecordedShutdownStartTime|
+ // here. This can happen during tests.
+ return;
+ }
+
+ nsTAutoString<PathChar> tmpName(name);
+ tmpName.AppendLiteral(".tmp");
+ RefPtr<nsLocalFile> tmpFile = new nsLocalFile(tmpName);
+ FILE* f;
+ if (NS_FAILED(tmpFile->OpenANSIFileDesc("w", &f)) || !f) return;
+ // On a normal release build this should be called just before
+ // calling _exit, but on a debug build or when the user forces a full
+ // shutdown this is called as late as possible, so we have to
+ // allow this write as write poisoning will be enabled.
+ MozillaRegisterDebugFILE(f);
+
+ TimeStamp now = TimeStamp::Now();
+ MOZ_ASSERT(now >= gRecordedShutdownStartTime);
+ TimeDuration diff = now - gRecordedShutdownStartTime;
+ uint32_t diff2 = diff.ToMilliseconds();
+ int written = fprintf(f, "%d\n", diff2);
+ MozillaUnRegisterDebugFILE(f);
+ int rv = fclose(f);
+ if (written < 0 || rv != 0) {
+ tmpFile->Remove(false);
+ return;
+ }
+ RefPtr<nsLocalFile> file = new nsLocalFile(name);
+ nsAutoString leafName;
+ file->GetLeafName(leafName);
+ tmpFile->RenameTo(nullptr, leafName);
+}
+
+} // namespace mozilla
+
+////////////////////////////////////////////////////////////////////////
+////////////////////////////////////////////////////////////////////////
+//
+// EXTERNALLY VISIBLE FUNCTIONS in mozilla::Telemetry::
+// These are listed in Telemetry.h
+
+namespace mozilla::Telemetry {
+
+// The external API for controlling recording state
+void SetHistogramRecordingEnabled(HistogramID aID, bool aEnabled) {
+ TelemetryHistogram::SetHistogramRecordingEnabled(aID, aEnabled);
+}
+
+void Accumulate(HistogramID aHistogram, uint32_t aSample) {
+ TelemetryHistogram::Accumulate(aHistogram, aSample);
+}
+
+void Accumulate(HistogramID aHistogram, const nsTArray<uint32_t>& aSamples) {
+ TelemetryHistogram::Accumulate(aHistogram, aSamples);
+}
+
+void Accumulate(HistogramID aID, const nsCString& aKey, uint32_t aSample) {
+ TelemetryHistogram::Accumulate(aID, aKey, aSample);
+}
+
+void Accumulate(HistogramID aID, const nsCString& aKey,
+ const nsTArray<uint32_t>& aSamples) {
+ TelemetryHistogram::Accumulate(aID, aKey, aSamples);
+}
+
+void Accumulate(const char* name, uint32_t sample) {
+ TelemetryHistogram::Accumulate(name, sample);
+}
+
+void Accumulate(const char* name, const nsCString& key, uint32_t sample) {
+ TelemetryHistogram::Accumulate(name, key, sample);
+}
+
+void AccumulateCategorical(HistogramID id, const nsCString& label) {
+ TelemetryHistogram::AccumulateCategorical(id, label);
+}
+
+void AccumulateCategorical(HistogramID id, const nsTArray<nsCString>& labels) {
+ TelemetryHistogram::AccumulateCategorical(id, labels);
+}
+
+void AccumulateTimeDelta(HistogramID aHistogram, TimeStamp start,
+ TimeStamp end) {
+ if (start > end) {
+ Accumulate(aHistogram, 0);
+ return;
+ }
+ Accumulate(aHistogram, static_cast<uint32_t>((end - start).ToMilliseconds()));
+}
+
+void AccumulateTimeDelta(HistogramID aHistogram, const nsCString& key,
+ TimeStamp start, TimeStamp end) {
+ if (start > end) {
+ Accumulate(aHistogram, key, 0);
+ return;
+ }
+ Accumulate(aHistogram, key,
+ static_cast<uint32_t>((end - start).ToMilliseconds()));
+}
+const char* GetHistogramName(HistogramID id) {
+ return TelemetryHistogram::GetHistogramName(id);
+}
+
+bool CanRecordBase() { return TelemetryImpl::CanRecordBase(); }
+
+bool CanRecordExtended() { return TelemetryImpl::CanRecordExtended(); }
+
+bool CanRecordReleaseData() { return TelemetryImpl::CanRecordReleaseData(); }
+
+bool CanRecordPrereleaseData() {
+ return TelemetryImpl::CanRecordPrereleaseData();
+}
+
+void RecordSlowSQLStatement(const nsACString& statement,
+ const nsACString& dbName, uint32_t delay) {
+ TelemetryImpl::RecordSlowStatement(statement, dbName, delay);
+}
+
+void Init() {
+ // Make the service manager hold a long-lived reference to the service
+ nsCOMPtr<nsITelemetry> telemetryService =
+ do_GetService("@mozilla.org/base/telemetry;1");
+ MOZ_ASSERT(telemetryService);
+}
+
+void WriteFailedProfileLock(nsIFile* aProfileDir) {
+ nsCOMPtr<nsIFile> file;
+ nsresult rv = GetFailedProfileLockFile(getter_AddRefs(file), aProfileDir);
+ NS_ENSURE_SUCCESS_VOID(rv);
+ int64_t fileSize = 0;
+ rv = file->GetFileSize(&fileSize);
+ // It's expected that the file might not exist yet
+ if (NS_FAILED(rv) && rv != NS_ERROR_FILE_NOT_FOUND) {
+ return;
+ }
+ nsCOMPtr<nsIRandomAccessStream> fileRandomAccessStream;
+ rv = NS_NewLocalFileRandomAccessStream(getter_AddRefs(fileRandomAccessStream),
+ file, PR_RDWR | PR_CREATE_FILE, 0640);
+ NS_ENSURE_SUCCESS_VOID(rv);
+ NS_ENSURE_TRUE_VOID(fileSize <= kMaxFailedProfileLockFileSize);
+ unsigned int failedLockCount = 0;
+ if (fileSize > 0) {
+ nsCOMPtr<nsIInputStream> inStream =
+ do_QueryInterface(fileRandomAccessStream);
+ NS_ENSURE_TRUE_VOID(inStream);
+ if (!GetFailedLockCount(inStream, fileSize, failedLockCount)) {
+ failedLockCount = 0;
+ }
+ }
+ ++failedLockCount;
+ nsAutoCString bufStr;
+ bufStr.AppendInt(static_cast<int>(failedLockCount));
+ // If we read in an existing failed lock count, we need to reset the file ptr
+ if (fileSize > 0) {
+ rv = fileRandomAccessStream->Seek(nsISeekableStream::NS_SEEK_SET, 0);
+ NS_ENSURE_SUCCESS_VOID(rv);
+ }
+ nsCOMPtr<nsIOutputStream> outStream =
+ do_QueryInterface(fileRandomAccessStream);
+ uint32_t bytesLeft = bufStr.Length();
+ const char* bytes = bufStr.get();
+ do {
+ uint32_t written = 0;
+ rv = outStream->Write(bytes, bytesLeft, &written);
+ if (NS_FAILED(rv)) {
+ break;
+ }
+ bytes += written;
+ bytesLeft -= written;
+ } while (bytesLeft > 0);
+ fileRandomAccessStream->SetEOF();
+}
+
+void InitIOReporting(nsIFile* aXreDir) {
+ // Never initialize twice
+ if (sTelemetryIOObserver) {
+ return;
+ }
+
+ sTelemetryIOObserver = new TelemetryIOInterposeObserver(aXreDir);
+ IOInterposer::Register(IOInterposeObserver::OpAllWithStaging,
+ sTelemetryIOObserver);
+}
+
+void SetProfileDir(nsIFile* aProfD) {
+ if (!sTelemetryIOObserver || !aProfD) {
+ return;
+ }
+ nsAutoString profDirPath;
+ nsresult rv = aProfD->GetPath(profDirPath);
+ if (NS_FAILED(rv)) {
+ return;
+ }
+ sTelemetryIOObserver->AddPath(profDirPath, u"{profile}"_ns);
+}
+
+// Scalar API C++ Endpoints
+
+void ScalarAdd(mozilla::Telemetry::ScalarID aId, uint32_t aVal) {
+ TelemetryScalar::Add(aId, aVal);
+}
+
+void ScalarSet(mozilla::Telemetry::ScalarID aId, uint32_t aVal) {
+ TelemetryScalar::Set(aId, aVal);
+}
+
+void ScalarSet(mozilla::Telemetry::ScalarID aId, bool aVal) {
+ TelemetryScalar::Set(aId, aVal);
+}
+
+void ScalarSet(mozilla::Telemetry::ScalarID aId, const nsAString& aVal) {
+ TelemetryScalar::Set(aId, aVal);
+}
+
+void ScalarSetMaximum(mozilla::Telemetry::ScalarID aId, uint32_t aVal) {
+ TelemetryScalar::SetMaximum(aId, aVal);
+}
+
+void ScalarAdd(mozilla::Telemetry::ScalarID aId, const nsAString& aKey,
+ uint32_t aVal) {
+ TelemetryScalar::Add(aId, aKey, aVal);
+}
+
+void ScalarSet(mozilla::Telemetry::ScalarID aId, const nsAString& aKey,
+ uint32_t aVal) {
+ TelemetryScalar::Set(aId, aKey, aVal);
+}
+
+void ScalarSet(mozilla::Telemetry::ScalarID aId, const nsAString& aKey,
+ bool aVal) {
+ TelemetryScalar::Set(aId, aKey, aVal);
+}
+
+void ScalarSetMaximum(mozilla::Telemetry::ScalarID aId, const nsAString& aKey,
+ uint32_t aVal) {
+ TelemetryScalar::SetMaximum(aId, aKey, aVal);
+}
+
+void RecordEvent(
+ mozilla::Telemetry::EventID aId, const mozilla::Maybe<nsCString>& aValue,
+ const mozilla::Maybe<CopyableTArray<EventExtraEntry>>& aExtra) {
+ TelemetryEvent::RecordEventNative(aId, aValue, aExtra);
+}
+
+void SetEventRecordingEnabled(const nsACString& aCategory, bool aEnabled) {
+ TelemetryEvent::SetEventRecordingEnabled(aCategory, aEnabled);
+}
+
+void ShutdownTelemetry() { TelemetryImpl::ShutdownTelemetry(); }
+
+} // namespace mozilla::Telemetry
+
+NS_IMPL_COMPONENT_FACTORY(nsITelemetry) {
+ return TelemetryImpl::CreateTelemetryInstance().downcast<nsISupports>();
+}
diff --git a/toolkit/components/telemetry/core/Telemetry.h b/toolkit/components/telemetry/core/Telemetry.h
new file mode 100644
index 0000000000..d0fb76a24e
--- /dev/null
+++ b/toolkit/components/telemetry/core/Telemetry.h
@@ -0,0 +1,577 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2; -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef Telemetry_h__
+#define Telemetry_h__
+
+#include "mozilla/Maybe.h"
+#include "mozilla/TelemetryEventEnums.h"
+#include "mozilla/TelemetryHistogramEnums.h"
+#include "mozilla/TelemetryScalarEnums.h"
+#include "mozilla/TimeStamp.h"
+#include "nsString.h"
+#include "nsTArray.h"
+#include "nsXULAppAPI.h"
+
+/******************************************************************************
+ * This implements the Telemetry system.
+ * It allows recording into histograms as well some more specialized data
+ * points and gives access to the data.
+ *
+ * For documentation on how to add and use new Telemetry probes, see:
+ * https://firefox-source-docs.mozilla.org/toolkit/components/telemetry/start/adding-a-new-probe.html
+ *
+ * For more general information on Telemetry see:
+ * https://wiki.mozilla.org/Telemetry
+ *****************************************************************************/
+
+namespace mozilla {
+namespace Telemetry {
+
+struct HistogramAccumulation;
+struct KeyedHistogramAccumulation;
+struct ScalarAction;
+struct KeyedScalarAction;
+struct ChildEventData;
+
+struct EventExtraEntry {
+ nsCString key;
+ nsCString value;
+};
+
+/**
+ * Initialize the Telemetry service on the main thread at startup.
+ */
+void Init();
+
+/**
+ * Shutdown the Telemetry service.
+ */
+void ShutdownTelemetry();
+
+/**
+ * Adds sample to a histogram defined in TelemetryHistogramEnums.h
+ *
+ * @param id - histogram id
+ * @param sample - value to record.
+ */
+void Accumulate(HistogramID id, uint32_t sample);
+
+/**
+ * Adds an array of samples to a histogram defined in TelemetryHistograms.h
+ * @param id - histogram id
+ * @param samples - values to record.
+ */
+void Accumulate(HistogramID id, const nsTArray<uint32_t>& samples);
+
+/**
+ * Adds sample to a keyed histogram defined in TelemetryHistogramEnums.h
+ *
+ * @param id - keyed histogram id
+ * @param key - the string key
+ * @param sample - (optional) value to record, defaults to 1.
+ */
+void Accumulate(HistogramID id, const nsCString& key, uint32_t sample = 1);
+
+/**
+ * Adds an array of samples to a histogram defined in TelemetryHistograms.h
+ * @param id - histogram id
+ * @param samples - values to record.
+ * @param key - the string key
+ */
+void Accumulate(HistogramID id, const nsCString& key,
+ const nsTArray<uint32_t>& samples);
+
+/**
+ * Adds a sample to a histogram defined in TelemetryHistogramEnums.h.
+ * This function is here to support telemetry measurements from Java,
+ * where we have only names and not numeric IDs. You should almost
+ * certainly be using the by-enum-id version instead of this one.
+ *
+ * @param name - histogram name
+ * @param sample - value to record
+ */
+void Accumulate(const char* name, uint32_t sample);
+
+/**
+ * Adds a sample to a histogram defined in TelemetryHistogramEnums.h.
+ * This function is here to support telemetry measurements from Java,
+ * where we have only names and not numeric IDs. You should almost
+ * certainly be using the by-enum-id version instead of this one.
+ *
+ * @param name - histogram name
+ * @param key - the string key
+ * @param sample - sample - (optional) value to record, defaults to 1.
+ */
+void Accumulate(const char* name, const nsCString& key, uint32_t sample = 1);
+
+/**
+ * Adds sample to a categorical histogram defined in TelemetryHistogramEnums.h
+ * This is the typesafe - and preferred - way to use the categorical histograms
+ * by passing values from the corresponding Telemetry::LABELS_* enum.
+ *
+ * @param enumValue - Label value from one of the Telemetry::LABELS_* enums.
+ */
+template <class E>
+void AccumulateCategorical(E enumValue) {
+ static_assert(IsCategoricalLabelEnum<E>::value,
+ "Only categorical label enum types are supported.");
+ Accumulate(static_cast<HistogramID>(CategoricalLabelId<E>::value),
+ static_cast<uint32_t>(enumValue));
+};
+
+/**
+ * Adds an array of samples to categorical histograms defined in
+ * TelemetryHistogramEnums.h This is the typesafe - and preferred - way to use
+ * the categorical histograms by passing values from the corresponding
+ * Telemetry::LABELS_* enums.
+ *
+ * @param enumValues - Array of labels from Telemetry::LABELS_* enums.
+ */
+template <class E>
+void AccumulateCategorical(const nsTArray<E>& enumValues) {
+ static_assert(IsCategoricalLabelEnum<E>::value,
+ "Only categorical label enum types are supported.");
+ nsTArray<uint32_t> intSamples(enumValues.Length());
+
+ for (E aValue : enumValues) {
+ intSamples.AppendElement(static_cast<uint32_t>(aValue));
+ }
+
+ HistogramID categoricalId =
+ static_cast<HistogramID>(CategoricalLabelId<E>::value);
+
+ Accumulate(categoricalId, intSamples);
+}
+
+/**
+ * Adds sample to a keyed categorical histogram defined in
+ * TelemetryHistogramEnums.h This is the typesafe - and preferred - way to use
+ * the keyed categorical histograms by passing values from the corresponding
+ * Telemetry::LABELS_* enum.
+ *
+ * @param key - the string key
+ * @param enumValue - Label value from one of the Telemetry::LABELS_* enums.
+ */
+template <class E>
+void AccumulateCategoricalKeyed(const nsCString& key, E enumValue) {
+ static_assert(IsCategoricalLabelEnum<E>::value,
+ "Only categorical label enum types are supported.");
+ Accumulate(static_cast<HistogramID>(CategoricalLabelId<E>::value), key,
+ static_cast<uint32_t>(enumValue));
+};
+
+/**
+ * Adds an array of samples to a keyed categorical histogram defined in
+ * TelemetryHistogramEnums.h. This is the typesafe - and preferred - way to use
+ * the keyed categorical histograms by passing values from the corresponding
+ * Telemetry::LABELS_*enum.
+ *
+ * @param key - the string key
+ * @param enumValue - Label value from one of the Telemetry::LABELS_* enums.
+ */
+template <class E>
+void AccumulateCategoricalKeyed(const nsCString& key,
+ const nsTArray<E>& enumValues) {
+ static_assert(IsCategoricalLabelEnum<E>::value,
+ "Only categorical label enum types are supported.");
+ nsTArray<uint32_t> intSamples(enumValues.Length());
+
+ for (E aValue : enumValues) {
+ intSamples.AppendElement(static_cast<uint32_t>(aValue));
+ }
+
+ Accumulate(static_cast<HistogramID>(CategoricalLabelId<E>::value), key,
+ intSamples);
+};
+
+/**
+ * Adds sample to a categorical histogram defined in TelemetryHistogramEnums.h
+ * This string will be matched against the labels defined in Histograms.json.
+ * If the string does not match a label defined for the histogram, nothing will
+ * be recorded.
+ *
+ * @param id - The histogram id.
+ * @param label - A string label value that is defined in Histograms.json for
+ * this histogram.
+ */
+void AccumulateCategorical(HistogramID id, const nsCString& label);
+
+/**
+ * Adds an array of samples to a categorical histogram defined in
+ * Histograms.json
+ *
+ * @param id - The histogram id
+ * @param labels - The array of labels to accumulate
+ */
+void AccumulateCategorical(HistogramID id, const nsTArray<nsCString>& labels);
+
+/**
+ * Adds time delta in milliseconds to a histogram defined in
+ * TelemetryHistogramEnums.h
+ *
+ * @param id - histogram id
+ * @param start - start time
+ * @param end - end time
+ */
+void AccumulateTimeDelta(HistogramID id, TimeStamp start,
+ TimeStamp end = TimeStamp::Now());
+
+/**
+ * Adds time delta in milliseconds to a keyed histogram defined in
+ * TelemetryHistogramEnums.h
+ *
+ * @param id - histogram id
+ * @param key - the string key
+ * @param start - start time
+ * @param end - end time
+ */
+void AccumulateTimeDelta(HistogramID id, const nsCString& key, TimeStamp start,
+ TimeStamp end = TimeStamp::Now());
+
+/**
+ * Enable/disable recording for this histogram in this process at runtime.
+ * Recording is enabled by default, unless listed at
+ * kRecordingInitiallyDisabledIDs[]. id must be a valid telemetry enum,
+ *
+ * @param id - histogram id
+ * @param enabled - whether or not to enable recording from now on.
+ */
+void SetHistogramRecordingEnabled(HistogramID id, bool enabled);
+
+const char* GetHistogramName(HistogramID id);
+
+class MOZ_RAII RuntimeAutoTimer {
+ public:
+ explicit RuntimeAutoTimer(Telemetry::HistogramID aId,
+ TimeStamp aStart = TimeStamp::Now())
+ : id(aId), start(aStart) {}
+ explicit RuntimeAutoTimer(Telemetry::HistogramID aId, const nsCString& aKey,
+ TimeStamp aStart = TimeStamp::Now())
+ : id(aId), key(aKey), start(aStart) {
+ MOZ_ASSERT(!aKey.IsEmpty(), "The key must not be empty.");
+ }
+
+ ~RuntimeAutoTimer() {
+ if (key.IsEmpty()) {
+ AccumulateTimeDelta(id, start);
+ } else {
+ AccumulateTimeDelta(id, key, start);
+ }
+ }
+
+ private:
+ Telemetry::HistogramID id;
+ const nsCString key;
+ const TimeStamp start;
+};
+
+template <HistogramID id>
+class MOZ_RAII AutoTimer {
+ public:
+ explicit AutoTimer(TimeStamp aStart = TimeStamp::Now()) : start(aStart) {}
+
+ explicit AutoTimer(const nsCString& aKey, TimeStamp aStart = TimeStamp::Now())
+ : start(aStart), key(aKey) {
+ MOZ_ASSERT(!aKey.IsEmpty(), "The key must not be empty.");
+ }
+
+ ~AutoTimer() {
+ if (key.IsEmpty()) {
+ AccumulateTimeDelta(id, start);
+ } else {
+ AccumulateTimeDelta(id, key, start);
+ }
+ }
+
+ private:
+ const TimeStamp start;
+ const nsCString key;
+};
+
+class MOZ_RAII RuntimeAutoCounter {
+ public:
+ explicit RuntimeAutoCounter(HistogramID aId, uint32_t counterStart = 0)
+ : id(aId), counter(counterStart) {}
+
+ ~RuntimeAutoCounter() { Accumulate(id, counter); }
+
+ // Prefix increment only, to encourage good habits.
+ void operator++() {
+ if (NS_WARN_IF(counter == std::numeric_limits<uint32_t>::max())) {
+ return;
+ }
+ ++counter;
+ }
+
+ // Chaining doesn't make any sense, don't return anything.
+ void operator+=(int increment) {
+ if (NS_WARN_IF(increment > 0 &&
+ static_cast<uint32_t>(increment) >
+ (std::numeric_limits<uint32_t>::max() - counter))) {
+ counter = std::numeric_limits<uint32_t>::max();
+ return;
+ }
+ if (NS_WARN_IF(increment < 0 &&
+ static_cast<uint32_t>(-increment) > counter)) {
+ counter = std::numeric_limits<uint32_t>::min();
+ return;
+ }
+ counter += increment;
+ }
+
+ private:
+ HistogramID id;
+ uint32_t counter;
+};
+
+template <HistogramID id>
+class MOZ_RAII AutoCounter {
+ public:
+ explicit AutoCounter(uint32_t counterStart = 0) : counter(counterStart) {}
+
+ ~AutoCounter() { Accumulate(id, counter); }
+
+ // Prefix increment only, to encourage good habits.
+ void operator++() {
+ if (NS_WARN_IF(counter == std::numeric_limits<uint32_t>::max())) {
+ return;
+ }
+ ++counter;
+ }
+
+ // Chaining doesn't make any sense, don't return anything.
+ void operator+=(int increment) {
+ if (NS_WARN_IF(increment > 0 &&
+ static_cast<uint32_t>(increment) >
+ (std::numeric_limits<uint32_t>::max() - counter))) {
+ counter = std::numeric_limits<uint32_t>::max();
+ return;
+ }
+ if (NS_WARN_IF(increment < 0 &&
+ static_cast<uint32_t>(-increment) > counter)) {
+ counter = std::numeric_limits<uint32_t>::min();
+ return;
+ }
+ counter += increment;
+ }
+
+ private:
+ uint32_t counter;
+};
+
+/**
+ * Indicates whether Telemetry base data recording is turned on. Added for
+ * future uses.
+ */
+bool CanRecordBase();
+
+/**
+ * Indicates whether Telemetry extended data recording is turned on. This is
+ * intended to guard calls to Accumulate when the statistic being recorded is
+ * expensive to compute.
+ */
+bool CanRecordExtended();
+
+/**
+ * Indicates whether Telemetry release data recording is turned on. Usually
+ * true.
+ *
+ * @see nsITelemetry.canRecordReleaseData
+ */
+bool CanRecordReleaseData();
+
+/**
+ * Indicates whether Telemetry pre-release data recording is turned on. Tends
+ * to be true on pre-release channels.
+ *
+ * @see nsITelemetry.canRecordPrereleaseData
+ */
+bool CanRecordPrereleaseData();
+
+/**
+ * Records slow SQL statements for Telemetry reporting.
+ *
+ * @param statement - offending SQL statement to record
+ * @param dbName - DB filename
+ * @param delay - execution time in milliseconds
+ */
+void RecordSlowSQLStatement(const nsACString& statement,
+ const nsACString& dbName, uint32_t delay);
+
+/**
+ * Initialize I/O Reporting
+ * Initially this only records I/O for files in the binary directory.
+ *
+ * @param aXreDir - XRE directory
+ */
+void InitIOReporting(nsIFile* aXreDir);
+
+/**
+ * Set the profile directory. Once called, files in the profile directory will
+ * be included in I/O reporting. We can't use the directory
+ * service to obtain this information because it isn't running yet.
+ */
+void SetProfileDir(nsIFile* aProfD);
+
+/**
+ * Called to inform Telemetry that startup has completed.
+ */
+void LeavingStartupStage();
+
+/**
+ * Called to inform Telemetry that shutdown is commencing.
+ */
+void EnteringShutdownStage();
+
+/**
+ * Thresholds for a statement to be considered slow, in milliseconds
+ */
+const uint32_t kSlowSQLThresholdForMainThread = 50;
+const uint32_t kSlowSQLThresholdForHelperThreads = 100;
+
+/**
+ * Record a failed attempt at locking the user's profile.
+ *
+ * @param aProfileDir The profile directory whose lock attempt failed
+ */
+void WriteFailedProfileLock(nsIFile* aProfileDir);
+
+/**
+ * Adds the value to the given scalar.
+ *
+ * @param aId The scalar enum id.
+ * @param aValue The value to add to the scalar.
+ */
+void ScalarAdd(mozilla::Telemetry::ScalarID aId, uint32_t aValue);
+
+/**
+ * Sets the scalar to the given value.
+ *
+ * @param aId The scalar enum id.
+ * @param aValue The value to set the scalar to.
+ */
+void ScalarSet(mozilla::Telemetry::ScalarID aId, uint32_t aValue);
+
+/**
+ * Sets the scalar to the given value.
+ *
+ * @param aId The scalar enum id.
+ * @param aValue The value to set the scalar to.
+ */
+void ScalarSet(mozilla::Telemetry::ScalarID aId, bool aValue);
+
+/**
+ * Sets the scalar to the given value.
+ *
+ * @param aId The scalar enum id.
+ * @param aValue The value to set the scalar to, truncated to
+ * 50 characters if exceeding that length.
+ */
+void ScalarSet(mozilla::Telemetry::ScalarID aId, const nsAString& aValue);
+
+/**
+ * Sets the scalar to the maximum of the current and the passed value.
+ *
+ * @param aId The scalar enum id.
+ * @param aValue The value the scalar is set to if its greater
+ * than the current value.
+ */
+void ScalarSetMaximum(mozilla::Telemetry::ScalarID aId, uint32_t aValue);
+
+/**
+ * Adds the value to the given scalar.
+ *
+ * @param aId The scalar enum id.
+ * @param aKey The scalar key.
+ * @param aValue The value to add to the scalar.
+ */
+void ScalarAdd(mozilla::Telemetry::ScalarID aId, const nsAString& aKey,
+ uint32_t aValue);
+
+/**
+ * Sets the scalar to the given value.
+ *
+ * @param aId The scalar enum id.
+ * @param aKey The scalar key.
+ * @param aValue The value to set the scalar to.
+ */
+void ScalarSet(mozilla::Telemetry::ScalarID aId, const nsAString& aKey,
+ uint32_t aValue);
+
+/**
+ * Sets the scalar to the given value.
+ *
+ * @param aId The scalar enum id.
+ * @param aKey The scalar key.
+ * @param aValue The value to set the scalar to.
+ */
+void ScalarSet(mozilla::Telemetry::ScalarID aId, const nsAString& aKey,
+ bool aValue);
+
+/**
+ * Sets the scalar to the maximum of the current and the passed value.
+ *
+ * @param aId The scalar enum id.
+ * @param aKey The scalar key.
+ * @param aValue The value the scalar is set to if its greater
+ * than the current value.
+ */
+void ScalarSetMaximum(mozilla::Telemetry::ScalarID aId, const nsAString& aKey,
+ uint32_t aValue);
+
+template <ScalarID id>
+class MOZ_RAII AutoScalarTimer {
+ public:
+ explicit AutoScalarTimer(TimeStamp aStart = TimeStamp::Now())
+ : start(aStart) {}
+
+ explicit AutoScalarTimer(const nsAString& aKey,
+ TimeStamp aStart = TimeStamp::Now())
+ : start(aStart), key(aKey) {
+ MOZ_ASSERT(!aKey.IsEmpty(), "The key must not be empty.");
+ }
+
+ ~AutoScalarTimer() {
+ TimeStamp end = TimeStamp::Now();
+ uint32_t delta = static_cast<uint32_t>((end - start).ToMilliseconds());
+ if (key.IsEmpty()) {
+ mozilla::Telemetry::ScalarSet(id, delta);
+ } else {
+ mozilla::Telemetry::ScalarSet(id, key, delta);
+ }
+ }
+
+ private:
+ const TimeStamp start;
+ const nsString key;
+};
+
+/**
+ * Records an event. See the Event documentation for more information:
+ * https://firefox-source-docs.mozilla.org/toolkit/components/telemetry/telemetry/collection/events.html
+ *
+ * @param aId The event enum id.
+ * @param aValue Optional. The event value.
+ * @param aExtra Optional. The event's extra key/value pairs.
+ */
+void RecordEvent(mozilla::Telemetry::EventID aId,
+ const mozilla::Maybe<nsCString>& aValue,
+ const mozilla::Maybe<CopyableTArray<EventExtraEntry>>& aExtra);
+
+/**
+ * Enables recording of events in a category.
+ * Events default to recording disabled.
+ * This toggles recording for all events in the specified category.
+ *
+ * @param aCategory The category name.
+ * @param aEnabled Whether recording should be enabled or disabled.
+ */
+void SetEventRecordingEnabled(const nsACString& aCategory, bool aEnabled);
+
+} // namespace Telemetry
+} // namespace mozilla
+
+#endif // Telemetry_h__
diff --git a/toolkit/components/telemetry/core/TelemetryCommon.cpp b/toolkit/components/telemetry/core/TelemetryCommon.cpp
new file mode 100644
index 0000000000..7113a682c9
--- /dev/null
+++ b/toolkit/components/telemetry/core/TelemetryCommon.cpp
@@ -0,0 +1,209 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "TelemetryCommon.h"
+
+#include <cstring>
+#include "js/String.h"
+#include "mozilla/TimeStamp.h"
+#include "mozilla/StaticPrefs_toolkit.h"
+#include "nsComponentManagerUtils.h"
+#include "nsIConsoleService.h"
+#include "nsITelemetry.h"
+#include "nsServiceManagerUtils.h"
+#include "nsThreadUtils.h"
+#include "nsVersionComparator.h"
+#include "TelemetryProcessData.h"
+#include "Telemetry.h"
+#include "mozilla/Uptime.h"
+
+namespace mozilla::Telemetry::Common {
+
+bool IsExpiredVersion(const char* aExpiration) {
+ MOZ_ASSERT(aExpiration);
+ // Note: We intentionally don't construct a static Version object here as we
+ // saw odd crashes around this (see bug 1334105).
+ return strcmp(aExpiration, "never") && strcmp(aExpiration, "default") &&
+ (mozilla::Version(aExpiration) <= MOZ_APP_VERSION);
+}
+
+bool IsInDataset(uint32_t aDataset, uint32_t aContainingDataset) {
+ if (aDataset == aContainingDataset) {
+ return true;
+ }
+
+ // The "optin on release channel" dataset is a superset of the
+ // "optout on release channel one".
+ if (aContainingDataset == nsITelemetry::DATASET_PRERELEASE_CHANNELS &&
+ aDataset == nsITelemetry::DATASET_ALL_CHANNELS) {
+ return true;
+ }
+
+ return false;
+}
+
+bool CanRecordDataset(uint32_t aDataset, bool aCanRecordBase,
+ bool aCanRecordExtended) {
+ // If we are extended telemetry is enabled, we are allowed to record
+ // regardless of the dataset.
+ if (aCanRecordExtended) {
+ return true;
+ }
+
+ // If base telemetry data is enabled and we're trying to record base
+ // telemetry, allow it.
+ if (aCanRecordBase &&
+ IsInDataset(aDataset, nsITelemetry::DATASET_ALL_CHANNELS)) {
+ return true;
+ }
+
+ // We're not recording extended telemetry or this is not the base
+ // dataset. Bail out.
+ return false;
+}
+
+bool CanRecordInProcess(RecordedProcessType processes,
+ GeckoProcessType processType) {
+ // We can use (1 << ProcessType) due to the way RecordedProcessType is
+ // defined.
+ bool canRecordProcess =
+ !!(processes & static_cast<RecordedProcessType>(1 << processType));
+
+ return canRecordProcess;
+}
+
+bool CanRecordInProcess(RecordedProcessType processes, ProcessID processId) {
+ return CanRecordInProcess(processes, GetGeckoProcessType(processId));
+}
+
+bool CanRecordProduct(SupportedProduct aProducts) {
+ return mozilla::StaticPrefs::
+ toolkit_telemetry_testing_overrideProductsCheck() ||
+ !!(aProducts & GetCurrentProduct());
+}
+
+nsresult MsSinceProcessStart(double* aResult) {
+ *aResult =
+ (TimeStamp::NowLoRes() - TimeStamp::ProcessCreation()).ToMilliseconds();
+ return NS_OK;
+}
+
+nsresult MsSinceProcessStartIncludingSuspend(double* aResult) {
+ auto rv = mozilla::ProcessUptimeMs();
+ if (rv) {
+ *aResult = rv.value();
+ return NS_OK;
+ }
+ return NS_ERROR_NOT_AVAILABLE;
+}
+
+nsresult MsSinceProcessStartExcludingSuspend(double* aResult) {
+ auto rv = mozilla::ProcessUptimeExcludingSuspendMs();
+ if (rv) {
+ *aResult = rv.value();
+ return NS_OK;
+ }
+ return NS_ERROR_NOT_AVAILABLE;
+}
+
+void LogToBrowserConsole(uint32_t aLogLevel, const nsAString& aMsg) {
+ if (!NS_IsMainThread()) {
+ nsString msg(aMsg);
+ nsCOMPtr<nsIRunnable> task = NS_NewRunnableFunction(
+ "Telemetry::Common::LogToBrowserConsole",
+ [aLogLevel, msg]() { LogToBrowserConsole(aLogLevel, msg); });
+ NS_DispatchToMainThread(task.forget(), NS_DISPATCH_NORMAL);
+ return;
+ }
+
+ nsCOMPtr<nsIConsoleService> console(
+ do_GetService("@mozilla.org/consoleservice;1"));
+ if (!console) {
+ NS_WARNING("Failed to log message to console.");
+ return;
+ }
+
+ nsCOMPtr<nsIScriptError> error(do_CreateInstance(NS_SCRIPTERROR_CONTRACTID));
+ error->Init(aMsg, u""_ns, u""_ns, 0, 0, aLogLevel, "chrome javascript"_ns,
+ false /* from private window */, true /* from chrome context */);
+ console->LogMessage(error);
+}
+
+const char* GetNameForProcessID(ProcessID process) {
+ MOZ_ASSERT(process < ProcessID::Count);
+ return ProcessIDToString[static_cast<uint32_t>(process)];
+}
+
+ProcessID GetIDForProcessName(const char* aProcessName) {
+ for (uint32_t id = 0; id < static_cast<uint32_t>(ProcessID::Count); id++) {
+ if (!strcmp(GetNameForProcessID(ProcessID(id)), aProcessName)) {
+ return ProcessID(id);
+ }
+ }
+
+ return ProcessID::Count;
+}
+
+GeckoProcessType GetGeckoProcessType(ProcessID process) {
+ MOZ_ASSERT(process < ProcessID::Count);
+ return ProcessIDToGeckoProcessType[static_cast<uint32_t>(process)];
+}
+
+bool IsStringCharValid(const char aChar, const bool aAllowInfixPeriod,
+ const bool aAllowInfixUnderscore) {
+ return (aChar >= 'A' && aChar <= 'Z') || (aChar >= 'a' && aChar <= 'z') ||
+ (aChar >= '0' && aChar <= '9') ||
+ (aAllowInfixPeriod && (aChar == '.')) ||
+ (aAllowInfixUnderscore && (aChar == '_'));
+}
+
+bool IsValidIdentifierString(const nsACString& aStr, const size_t aMaxLength,
+ const bool aAllowInfixPeriod,
+ const bool aAllowInfixUnderscore) {
+ // Check string length.
+ if (aStr.Length() > aMaxLength) {
+ return false;
+ }
+
+ // Check string characters.
+ const char* first = aStr.BeginReading();
+ const char* end = aStr.EndReading();
+
+ for (const char* cur = first; cur < end; ++cur) {
+ const bool infix = (cur != first) && (cur != (end - 1));
+ if (!IsStringCharValid(*cur, aAllowInfixPeriod && infix,
+ aAllowInfixUnderscore && infix)) {
+ return false;
+ }
+ }
+
+ return true;
+}
+
+JSString* ToJSString(JSContext* cx, const nsACString& aStr) {
+ const NS_ConvertUTF8toUTF16 wide(aStr);
+ return JS_NewUCStringCopyN(cx, wide.Data(), wide.Length());
+}
+
+JSString* ToJSString(JSContext* cx, const nsAString& aStr) {
+ return JS_NewUCStringCopyN(cx, aStr.Data(), aStr.Length());
+}
+
+SupportedProduct GetCurrentProduct() {
+#if defined(MOZ_WIDGET_ANDROID)
+ if (mozilla::StaticPrefs::toolkit_telemetry_geckoview_streaming()) {
+ return SupportedProduct::GeckoviewStreaming;
+ } else {
+ return SupportedProduct::Fennec;
+ }
+#elif defined(MOZ_THUNDERBIRD)
+ return SupportedProduct::Thunderbird;
+#else
+ return SupportedProduct::Firefox;
+#endif
+}
+
+} // namespace mozilla::Telemetry::Common
diff --git a/toolkit/components/telemetry/core/TelemetryCommon.h b/toolkit/components/telemetry/core/TelemetryCommon.h
new file mode 100644
index 0000000000..141410f150
--- /dev/null
+++ b/toolkit/components/telemetry/core/TelemetryCommon.h
@@ -0,0 +1,198 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2; -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef TelemetryCommon_h__
+#define TelemetryCommon_h__
+
+#include "PLDHashTable.h"
+#include "js/RootingAPI.h"
+#include "js/TypeDecls.h"
+#include "mozilla/TypedEnumBits.h"
+#include "mozilla/TelemetryProcessEnums.h"
+#include "nsHashtablesFwd.h"
+#include "nsTHashSet.h"
+#include "nsTHashtable.h"
+#include "nsIScriptError.h"
+#include "nsXULAppAPI.h"
+
+namespace mozilla {
+namespace Telemetry {
+namespace Common {
+
+typedef nsTHashSet<nsCString> StringHashSet;
+
+enum class RecordedProcessType : uint16_t {
+ Main = (1 << GeckoProcessType_Default), // Also known as "parent process"
+ Content = (1 << GeckoProcessType_Content),
+ Gpu = (1 << GeckoProcessType_GPU),
+ Rdd = (1 << GeckoProcessType_RDD),
+ Socket = (1 << GeckoProcessType_Socket),
+ Utility = (1 << GeckoProcessType_Utility),
+ AllChildren = 0xFFFF - 1, // All the child processes (i.e. content, gpu, ...)
+ // Always `All-Main` to allow easy matching.
+ All = 0xFFFF // All the processes
+};
+MOZ_MAKE_ENUM_CLASS_BITWISE_OPERATORS(RecordedProcessType);
+static_assert(static_cast<uint16_t>(RecordedProcessType::Main) == 1,
+ "Main process type must be equal to 1 to allow easy matching in "
+ "CanRecordInProcess");
+
+enum class SupportedProduct : uint8_t {
+ Firefox = (1 << 0),
+ Fennec = (1 << 1),
+ // Note that `1 << 2` (former GeckoView) is missing in the representation
+ // but isn't necessary to be maintained, but we see no point in filling it
+ // at this time.
+ GeckoviewStreaming = (1 << 3),
+ Thunderbird = (1 << 4),
+};
+MOZ_MAKE_ENUM_CLASS_BITWISE_OPERATORS(SupportedProduct);
+
+template <class EntryType>
+class AutoHashtable : public nsTHashtable<EntryType> {
+ public:
+ explicit AutoHashtable(
+ uint32_t initLength = PLDHashTable::kDefaultInitialLength);
+ typedef bool (*ReflectEntryFunc)(EntryType* entry, JSContext* cx,
+ JS::Handle<JSObject*> obj);
+ bool ReflectIntoJS(ReflectEntryFunc entryFunc, JSContext* cx,
+ JS::Handle<JSObject*> obj);
+};
+
+template <class EntryType>
+AutoHashtable<EntryType>::AutoHashtable(uint32_t initLength)
+ : nsTHashtable<EntryType>(initLength) {}
+
+/**
+ * Reflect the individual entries of table into JS, usually by defining
+ * some property and value of obj. entryFunc is called for each entry.
+ */
+template <typename EntryType>
+bool AutoHashtable<EntryType>::ReflectIntoJS(ReflectEntryFunc entryFunc,
+ JSContext* cx,
+ JS::Handle<JSObject*> obj) {
+ for (auto iter = this->Iter(); !iter.Done(); iter.Next()) {
+ if (!entryFunc(iter.Get(), cx, obj)) {
+ return false;
+ }
+ }
+ return true;
+}
+
+bool IsExpiredVersion(const char* aExpiration);
+bool IsInDataset(uint32_t aDataset, uint32_t aContainingDataset);
+bool CanRecordDataset(uint32_t aDataset, bool aCanRecordBase,
+ bool aCanRecordExtended);
+bool CanRecordInProcess(RecordedProcessType aProcesses,
+ GeckoProcessType aProcess);
+bool CanRecordInProcess(RecordedProcessType aProcesses, ProcessID aProcess);
+bool CanRecordProduct(SupportedProduct aProducts);
+
+/**
+ * Return the number of milliseconds since process start using monotonic
+ * timestamps (unaffected by system clock changes). Depending on the platform,
+ * this can include the time the device was suspended (Windows) or not (Linux,
+ * macOS).
+ *
+ * @return NS_OK on success.
+ */
+nsresult MsSinceProcessStart(double* aResult);
+
+/**
+ * Return the number of milliseconds since process start using monotonic
+ * timestamps (unaffected by system clock changes), including the time the
+ * system was suspended.
+ *
+ * @return NS_OK on success, NS_ERROR_NOT_AVAILABLE if the data is unavailable
+ * (this can happen on old operating systems).
+ */
+nsresult MsSinceProcessStartIncludingSuspend(double* aResult);
+
+/**
+ * Return the number of milliseconds since process start using monotonic
+ * timestamps (unaffected by system clock changes), excluding the time the
+ * system was suspended.
+ *
+ * @return NS_OK on success, NS_ERROR_NOT_AVAILABLE if the data is unavailable
+ * (this can happen on old operating systems).
+ */
+nsresult MsSinceProcessStartExcludingSuspend(double* aResult);
+
+/**
+ * Dumps a log message to the Browser Console using the provided level.
+ *
+ * @param aLogLevel The level to use when displaying the message in the browser
+ * console (e.g. nsIScriptError::warningFlag, ...).
+ * @param aMsg The text message to print to the console.
+ */
+void LogToBrowserConsole(uint32_t aLogLevel, const nsAString& aMsg);
+
+/**
+ * Get the name string for a ProcessID.
+ * This is the name we use for the Telemetry payloads.
+ */
+const char* GetNameForProcessID(ProcessID process);
+
+/**
+ * Get the process id give a process name.
+ *
+ * @param aProcessName - the name of the process.
+ * @returns {ProcessID} one value from ProcessID::* or ProcessID::Count if the
+ * name of the process was not found.
+ */
+ProcessID GetIDForProcessName(const char* aProcessName);
+
+/**
+ * Get the GeckoProcessType for a ProcessID.
+ * Telemetry distinguishes between more process types than the GeckoProcessType,
+ * so the mapping is not direct.
+ */
+GeckoProcessType GetGeckoProcessType(ProcessID process);
+
+/**
+ * Check if the passed telemetry identifier is valid.
+ *
+ * @param aStr The string identifier.
+ * @param aMaxLength The maximum length of the identifier.
+ * @param aAllowInfixPeriod Whether or not to allow infix dots.
+ * @param aAllowInfixUnderscore Whether or not to allow infix underscores.
+ * @returns true if the string validates correctly, false otherwise.
+ */
+bool IsValidIdentifierString(const nsACString& aStr, const size_t aMaxLength,
+ const bool aAllowInfixPeriod,
+ const bool aAllowInfixUnderscore);
+
+/**
+ * Convert the given UTF8 string to a JavaScript string. The returned
+ * string's contents will be the UTF16 conversion of the given string.
+ *
+ * @param cx The JS context.
+ * @param aStr The UTF8 string.
+ * @returns a JavaScript string.
+ */
+JSString* ToJSString(JSContext* cx, const nsACString& aStr);
+
+/**
+ * Convert the given UTF16 string to a JavaScript string.
+ *
+ * @param cx The JS context.
+ * @param aStr The UTF16 string.
+ * @returns a JavaScript string.
+ */
+JSString* ToJSString(JSContext* cx, const nsAString& aStr);
+
+/**
+ * Get an identifier for the currently-running product.
+ * This is not stable over time and may change.
+ *
+ * @returns the product identifier
+ */
+SupportedProduct GetCurrentProduct();
+
+} // namespace Common
+} // namespace Telemetry
+} // namespace mozilla
+
+#endif // TelemetryCommon_h__
diff --git a/toolkit/components/telemetry/core/TelemetryEvent.cpp b/toolkit/components/telemetry/core/TelemetryEvent.cpp
new file mode 100644
index 0000000000..ad6a3fd2c7
--- /dev/null
+++ b/toolkit/components/telemetry/core/TelemetryEvent.cpp
@@ -0,0 +1,1387 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "Telemetry.h"
+#include "TelemetryEvent.h"
+#include <limits>
+#include "ipc/TelemetryIPCAccumulator.h"
+#include "jsapi.h"
+#include "js/Array.h" // JS::GetArrayLength, JS::IsArrayObject, JS::NewArrayObject
+#include "js/PropertyAndElement.h" // JS_DefineElement, JS_DefineProperty, JS_Enumerate, JS_GetElement, JS_GetProperty, JS_GetPropertyById, JS_HasProperty
+#include "mozilla/Maybe.h"
+#include "mozilla/Services.h"
+#include "mozilla/StaticMutex.h"
+#include "mozilla/StaticPtr.h"
+#include "nsClassHashtable.h"
+#include "nsHashKeys.h"
+#include "nsIObserverService.h"
+#include "nsITelemetry.h"
+#include "nsJSUtils.h"
+#include "nsPrintfCString.h"
+#include "nsTArray.h"
+#include "nsUTF8Utils.h"
+#include "nsXULAppAPI.h"
+#include "TelemetryCommon.h"
+#include "TelemetryEventData.h"
+#include "TelemetryScalar.h"
+
+using mozilla::MakeUnique;
+using mozilla::Maybe;
+using mozilla::StaticAutoPtr;
+using mozilla::StaticMutex;
+using mozilla::StaticMutexAutoLock;
+using mozilla::TimeStamp;
+using mozilla::UniquePtr;
+using mozilla::Telemetry::ChildEventData;
+using mozilla::Telemetry::EventExtraEntry;
+using mozilla::Telemetry::LABELS_TELEMETRY_EVENT_RECORDING_ERROR;
+using mozilla::Telemetry::LABELS_TELEMETRY_EVENT_REGISTRATION_ERROR;
+using mozilla::Telemetry::ProcessID;
+using mozilla::Telemetry::Common::CanRecordDataset;
+using mozilla::Telemetry::Common::CanRecordInProcess;
+using mozilla::Telemetry::Common::CanRecordProduct;
+using mozilla::Telemetry::Common::GetNameForProcessID;
+using mozilla::Telemetry::Common::IsExpiredVersion;
+using mozilla::Telemetry::Common::IsInDataset;
+using mozilla::Telemetry::Common::IsValidIdentifierString;
+using mozilla::Telemetry::Common::LogToBrowserConsole;
+using mozilla::Telemetry::Common::MsSinceProcessStart;
+using mozilla::Telemetry::Common::ToJSString;
+
+namespace TelemetryIPCAccumulator = mozilla::TelemetryIPCAccumulator;
+
+////////////////////////////////////////////////////////////////////////
+////////////////////////////////////////////////////////////////////////
+//
+// Naming: there are two kinds of functions in this file:
+//
+// * Functions taking a StaticMutexAutoLock: these can only be reached via
+// an interface function (TelemetryEvent::*). They expect the interface
+// function to have acquired |gTelemetryEventsMutex|, so they do not
+// have to be thread-safe.
+//
+// * Functions named TelemetryEvent::*. This is the external interface.
+// Entries and exits to these functions are serialised using
+// |gTelemetryEventsMutex|.
+//
+// Avoiding races and deadlocks:
+//
+// All functions in the external interface (TelemetryEvent::*) are
+// serialised using the mutex |gTelemetryEventsMutex|. This means
+// that the external interface is thread-safe, and the internal
+// functions can ignore thread safety. But it also brings a danger
+// of deadlock if any function in the external interface can get back
+// to that interface. That is, we will deadlock on any call chain like
+// this:
+//
+// TelemetryEvent::* -> .. any functions .. -> TelemetryEvent::*
+//
+// To reduce the danger of that happening, observe the following rules:
+//
+// * No function in TelemetryEvent::* may directly call, nor take the
+// address of, any other function in TelemetryEvent::*.
+//
+// * No internal function may call, nor take the address
+// of, any function in TelemetryEvent::*.
+
+////////////////////////////////////////////////////////////////////////
+////////////////////////////////////////////////////////////////////////
+//
+// PRIVATE TYPES
+
+namespace {
+
+const uint32_t kEventCount =
+ static_cast<uint32_t>(mozilla::Telemetry::EventID::EventCount);
+// This is a special event id used to mark expired events, to make expiry checks
+// cheap at runtime.
+const uint32_t kExpiredEventId = std::numeric_limits<uint32_t>::max();
+static_assert(kExpiredEventId > kEventCount,
+ "Built-in event count should be less than the expired event id.");
+
+// Maximum length of any passed value string, in UTF8 byte sequence length.
+const uint32_t kMaxValueByteLength = 80;
+// Maximum length of any string value in the extra dictionary, in UTF8 byte
+// sequence length.
+const uint32_t kMaxExtraValueByteLength = 80;
+// Maximum length of dynamic method names, in UTF8 byte sequence length.
+const uint32_t kMaxMethodNameByteLength = 20;
+// Maximum length of dynamic object names, in UTF8 byte sequence length.
+const uint32_t kMaxObjectNameByteLength = 20;
+// Maximum length of extra key names, in UTF8 byte sequence length.
+const uint32_t kMaxExtraKeyNameByteLength = 15;
+// The maximum number of valid extra keys for an event.
+const uint32_t kMaxExtraKeyCount = 10;
+// The number of event records allowed in an event ping.
+const uint32_t kEventPingLimit = 1000;
+
+struct EventKey {
+ uint32_t id;
+ bool dynamic;
+
+ EventKey() : id(kExpiredEventId), dynamic(false) {}
+ EventKey(uint32_t id_, bool dynamic_) : id(id_), dynamic(dynamic_) {}
+};
+
+struct DynamicEventInfo {
+ DynamicEventInfo(const nsACString& category, const nsACString& method,
+ const nsACString& object, nsTArray<nsCString>& extra_keys,
+ bool recordOnRelease, bool builtin)
+ : category(category),
+ method(method),
+ object(object),
+ extra_keys(extra_keys.Clone()),
+ recordOnRelease(recordOnRelease),
+ builtin(builtin) {}
+
+ DynamicEventInfo(const DynamicEventInfo&) = default;
+ DynamicEventInfo& operator=(const DynamicEventInfo&) = delete;
+
+ const nsCString category;
+ const nsCString method;
+ const nsCString object;
+ const CopyableTArray<nsCString> extra_keys;
+ const bool recordOnRelease;
+ const bool builtin;
+
+ size_t SizeOfExcludingThis(mozilla::MallocSizeOf aMallocSizeOf) const {
+ size_t n = 0;
+
+ n += category.SizeOfExcludingThisIfUnshared(aMallocSizeOf);
+ n += method.SizeOfExcludingThisIfUnshared(aMallocSizeOf);
+ n += object.SizeOfExcludingThisIfUnshared(aMallocSizeOf);
+ n += extra_keys.ShallowSizeOfExcludingThis(aMallocSizeOf);
+ for (auto& key : extra_keys) {
+ n += key.SizeOfExcludingThisIfUnshared(aMallocSizeOf);
+ }
+
+ return n;
+ }
+};
+
+enum class RecordEventResult {
+ Ok,
+ UnknownEvent,
+ InvalidExtraKey,
+ StorageLimitReached,
+ ExpiredEvent,
+ WrongProcess,
+ CannotRecord,
+};
+
+typedef CopyableTArray<EventExtraEntry> ExtraArray;
+
+class EventRecord {
+ public:
+ EventRecord(double timestamp, const EventKey& key,
+ const Maybe<nsCString>& value, const ExtraArray& extra)
+ : mTimestamp(timestamp),
+ mEventKey(key),
+ mValue(value),
+ mExtra(extra.Clone()) {}
+
+ EventRecord(const EventRecord& other) = default;
+
+ EventRecord& operator=(const EventRecord& other) = delete;
+
+ double Timestamp() const { return mTimestamp; }
+ const EventKey& GetEventKey() const { return mEventKey; }
+ const Maybe<nsCString>& Value() const { return mValue; }
+ const ExtraArray& Extra() const { return mExtra; }
+
+ size_t SizeOfExcludingThis(mozilla::MallocSizeOf aMallocSizeOf) const;
+
+ private:
+ const double mTimestamp;
+ const EventKey mEventKey;
+ const Maybe<nsCString> mValue;
+ const ExtraArray mExtra;
+};
+
+// Implements the methods for EventInfo.
+const nsDependentCString EventInfo::method() const {
+ return nsDependentCString(&gEventsStringTable[this->method_offset]);
+}
+
+const nsDependentCString EventInfo::object() const {
+ return nsDependentCString(&gEventsStringTable[this->object_offset]);
+}
+
+// Implements the methods for CommonEventInfo.
+const nsDependentCString CommonEventInfo::category() const {
+ return nsDependentCString(&gEventsStringTable[this->category_offset]);
+}
+
+const nsDependentCString CommonEventInfo::expiration_version() const {
+ return nsDependentCString(
+ &gEventsStringTable[this->expiration_version_offset]);
+}
+
+const nsDependentCString CommonEventInfo::extra_key(uint32_t index) const {
+ MOZ_ASSERT(index < this->extra_count);
+ uint32_t key_index = gExtraKeysTable[this->extra_index + index];
+ return nsDependentCString(&gEventsStringTable[key_index]);
+}
+
+// Implementation for the EventRecord class.
+size_t EventRecord::SizeOfExcludingThis(
+ mozilla::MallocSizeOf aMallocSizeOf) const {
+ size_t n = 0;
+
+ if (mValue) {
+ n += mValue.value().SizeOfExcludingThisIfUnshared(aMallocSizeOf);
+ }
+
+ n += mExtra.ShallowSizeOfExcludingThis(aMallocSizeOf);
+ for (uint32_t i = 0; i < mExtra.Length(); ++i) {
+ n += mExtra[i].key.SizeOfExcludingThisIfUnshared(aMallocSizeOf);
+ n += mExtra[i].value.SizeOfExcludingThisIfUnshared(aMallocSizeOf);
+ }
+
+ return n;
+}
+
+nsCString UniqueEventName(const nsACString& category, const nsACString& method,
+ const nsACString& object) {
+ nsCString name;
+ name.Append(category);
+ name.AppendLiteral("#");
+ name.Append(method);
+ name.AppendLiteral("#");
+ name.Append(object);
+ return name;
+}
+
+nsCString UniqueEventName(const EventInfo& info) {
+ return UniqueEventName(info.common_info.category(), info.method(),
+ info.object());
+}
+
+nsCString UniqueEventName(const DynamicEventInfo& info) {
+ return UniqueEventName(info.category, info.method, info.object);
+}
+
+void TruncateToByteLength(nsCString& str, uint32_t length) {
+ // last will be the index of the first byte of the current multi-byte
+ // sequence.
+ uint32_t last = RewindToPriorUTF8Codepoint(str.get(), length);
+ str.Truncate(last);
+}
+
+} // anonymous namespace
+
+////////////////////////////////////////////////////////////////////////
+////////////////////////////////////////////////////////////////////////
+//
+// PRIVATE STATE, SHARED BY ALL THREADS
+
+namespace {
+
+// Set to true once this global state has been initialized.
+bool gInitDone = false;
+
+bool gCanRecordBase;
+bool gCanRecordExtended;
+
+// The EventName -> EventKey cache map.
+nsTHashMap<nsCStringHashKey, EventKey> gEventNameIDMap(kEventCount);
+
+// The CategoryName set.
+nsTHashSet<nsCString> gCategoryNames;
+
+// This tracks the IDs of the categories for which recording is enabled.
+nsTHashSet<nsCString> gEnabledCategories;
+
+// The main event storage. Events are inserted here, keyed by process id and
+// in recording order.
+typedef nsUint32HashKey ProcessIDHashKey;
+typedef nsTArray<EventRecord> EventRecordArray;
+typedef nsClassHashtable<ProcessIDHashKey, EventRecordArray>
+ EventRecordsMapType;
+
+EventRecordsMapType gEventRecords;
+
+// The details on dynamic events that are recorded from addons are registered
+// here.
+StaticAutoPtr<nsTArray<DynamicEventInfo>> gDynamicEventInfo;
+
+} // namespace
+
+////////////////////////////////////////////////////////////////////////
+////////////////////////////////////////////////////////////////////////
+//
+// PRIVATE: thread-safe helpers for event recording.
+
+namespace {
+
+unsigned int GetDataset(const StaticMutexAutoLock& lock,
+ const EventKey& eventKey) {
+ if (!eventKey.dynamic) {
+ return gEventInfo[eventKey.id].common_info.dataset;
+ }
+
+ if (!gDynamicEventInfo) {
+ return nsITelemetry::DATASET_PRERELEASE_CHANNELS;
+ }
+
+ return (*gDynamicEventInfo)[eventKey.id].recordOnRelease
+ ? nsITelemetry::DATASET_ALL_CHANNELS
+ : nsITelemetry::DATASET_PRERELEASE_CHANNELS;
+}
+
+nsCString GetCategory(const StaticMutexAutoLock& lock,
+ const EventKey& eventKey) {
+ if (!eventKey.dynamic) {
+ return gEventInfo[eventKey.id].common_info.category();
+ }
+
+ if (!gDynamicEventInfo) {
+ return ""_ns;
+ }
+
+ return (*gDynamicEventInfo)[eventKey.id].category;
+}
+
+bool CanRecordEvent(const StaticMutexAutoLock& lock, const EventKey& eventKey,
+ ProcessID process) {
+ if (!gCanRecordBase) {
+ return false;
+ }
+
+ if (!CanRecordDataset(GetDataset(lock, eventKey), gCanRecordBase,
+ gCanRecordExtended)) {
+ return false;
+ }
+
+ // We don't allow specifying a process to record in for dynamic events.
+ if (!eventKey.dynamic) {
+ const CommonEventInfo& info = gEventInfo[eventKey.id].common_info;
+
+ if (!CanRecordProduct(info.products)) {
+ return false;
+ }
+
+ if (!CanRecordInProcess(info.record_in_processes, process)) {
+ return false;
+ }
+ }
+
+ return true;
+}
+
+bool IsExpired(const EventKey& key) { return key.id == kExpiredEventId; }
+
+EventRecordArray* GetEventRecordsForProcess(const StaticMutexAutoLock& lock,
+ ProcessID processType) {
+ return gEventRecords.GetOrInsertNew(uint32_t(processType));
+}
+
+bool GetEventKey(const StaticMutexAutoLock& lock, const nsACString& category,
+ const nsACString& method, const nsACString& object,
+ EventKey* aEventKey) {
+ const nsCString& name = UniqueEventName(category, method, object);
+ return gEventNameIDMap.Get(name, aEventKey);
+}
+
+static bool CheckExtraKeysValid(const EventKey& eventKey,
+ const ExtraArray& extra) {
+ nsTHashSet<nsCString> validExtraKeys;
+ if (!eventKey.dynamic) {
+ const CommonEventInfo& common = gEventInfo[eventKey.id].common_info;
+ for (uint32_t i = 0; i < common.extra_count; ++i) {
+ validExtraKeys.Insert(common.extra_key(i));
+ }
+ } else if (gDynamicEventInfo) {
+ const DynamicEventInfo& info = (*gDynamicEventInfo)[eventKey.id];
+ for (uint32_t i = 0, len = info.extra_keys.Length(); i < len; ++i) {
+ validExtraKeys.Insert(info.extra_keys[i]);
+ }
+ }
+
+ for (uint32_t i = 0; i < extra.Length(); ++i) {
+ if (!validExtraKeys.Contains(extra[i].key)) {
+ return false;
+ }
+ }
+
+ return true;
+}
+
+RecordEventResult RecordEvent(const StaticMutexAutoLock& lock,
+ ProcessID processType, double timestamp,
+ const nsACString& category,
+ const nsACString& method,
+ const nsACString& object,
+ const Maybe<nsCString>& value,
+ const ExtraArray& extra) {
+ // Look up the event id.
+ EventKey eventKey;
+ if (!GetEventKey(lock, category, method, object, &eventKey)) {
+ mozilla::Telemetry::AccumulateCategorical(
+ LABELS_TELEMETRY_EVENT_RECORDING_ERROR::UnknownEvent);
+ return RecordEventResult::UnknownEvent;
+ }
+
+ // If the event is expired or not enabled for this process, we silently drop
+ // this call. We don't want recording for expired probes to be an error so
+ // code doesn't have to be removed at a specific time or version. Even logging
+ // warnings would become very noisy.
+ if (IsExpired(eventKey)) {
+ mozilla::Telemetry::AccumulateCategorical(
+ LABELS_TELEMETRY_EVENT_RECORDING_ERROR::Expired);
+ return RecordEventResult::ExpiredEvent;
+ }
+
+ // Fixup the process id only for non-builtin (e.g. supporting build faster)
+ // dynamic events.
+ auto dynamicNonBuiltin =
+ eventKey.dynamic && !(*gDynamicEventInfo)[eventKey.id].builtin;
+ if (dynamicNonBuiltin) {
+ processType = ProcessID::Dynamic;
+ }
+
+ // Check whether the extra keys passed are valid.
+ if (!CheckExtraKeysValid(eventKey, extra)) {
+ mozilla::Telemetry::AccumulateCategorical(
+ LABELS_TELEMETRY_EVENT_RECORDING_ERROR::ExtraKey);
+ return RecordEventResult::InvalidExtraKey;
+ }
+
+ // Check whether we can record this event.
+ if (!CanRecordEvent(lock, eventKey, processType)) {
+ return RecordEventResult::CannotRecord;
+ }
+
+ // Count the number of times this event has been recorded, even if its
+ // category does not have recording enabled.
+ TelemetryScalar::SummarizeEvent(UniqueEventName(category, method, object),
+ processType, dynamicNonBuiltin);
+
+ // Check whether this event's category has recording enabled
+ if (!gEnabledCategories.Contains(GetCategory(lock, eventKey))) {
+ return RecordEventResult::Ok;
+ }
+
+ EventRecordArray* eventRecords = GetEventRecordsForProcess(lock, processType);
+ eventRecords->AppendElement(EventRecord(timestamp, eventKey, value, extra));
+
+ // Notify observers when we hit the "event" ping event record limit.
+ if (eventRecords->Length() == kEventPingLimit) {
+ return RecordEventResult::StorageLimitReached;
+ }
+
+ return RecordEventResult::Ok;
+}
+
+RecordEventResult ShouldRecordChildEvent(const StaticMutexAutoLock& lock,
+ const nsACString& category,
+ const nsACString& method,
+ const nsACString& object) {
+ EventKey eventKey;
+ if (!GetEventKey(lock, category, method, object, &eventKey)) {
+ // This event is unknown in this process, but it might be a dynamic event
+ // that was registered in the parent process.
+ return RecordEventResult::Ok;
+ }
+
+ if (IsExpired(eventKey)) {
+ return RecordEventResult::ExpiredEvent;
+ }
+
+ const auto processes =
+ gEventInfo[eventKey.id].common_info.record_in_processes;
+ if (!CanRecordInProcess(processes, XRE_GetProcessType())) {
+ return RecordEventResult::WrongProcess;
+ }
+
+ return RecordEventResult::Ok;
+}
+
+void RegisterEvents(const StaticMutexAutoLock& lock, const nsACString& category,
+ const nsTArray<DynamicEventInfo>& eventInfos,
+ const nsTArray<bool>& eventExpired, bool aBuiltin) {
+ MOZ_ASSERT(eventInfos.Length() == eventExpired.Length(),
+ "Event data array sizes should match.");
+
+ // Register the new events.
+ if (!gDynamicEventInfo) {
+ gDynamicEventInfo = new nsTArray<DynamicEventInfo>();
+ }
+
+ for (uint32_t i = 0, len = eventInfos.Length(); i < len; ++i) {
+ const nsCString& eventName = UniqueEventName(eventInfos[i]);
+
+ // Re-registering events can happen for two reasons and we don't print
+ // warnings:
+ //
+ // * When add-ons update.
+ // We don't support changing their definition, but the expiry might have
+ // changed.
+ // * When dynamic builtins ("build faster") events are registered.
+ // The dynamic definition takes precedence then.
+ EventKey existing;
+ if (!aBuiltin && gEventNameIDMap.Get(eventName, &existing)) {
+ if (eventExpired[i]) {
+ existing.id = kExpiredEventId;
+ }
+ continue;
+ }
+
+ gDynamicEventInfo->AppendElement(eventInfos[i]);
+ uint32_t eventId =
+ eventExpired[i] ? kExpiredEventId : gDynamicEventInfo->Length() - 1;
+ gEventNameIDMap.InsertOrUpdate(eventName, EventKey{eventId, true});
+ }
+
+ // If it is a builtin, add the category name in order to enable it later.
+ if (aBuiltin) {
+ gCategoryNames.Insert(category);
+ }
+
+ if (!aBuiltin) {
+ // Now after successful registration enable recording for this category
+ // (if not a dynamic builtin).
+ gEnabledCategories.Insert(category);
+ }
+}
+
+} // anonymous namespace
+
+////////////////////////////////////////////////////////////////////////
+////////////////////////////////////////////////////////////////////////
+//
+// PRIVATE: thread-unsafe helpers for event handling.
+
+namespace {
+
+nsresult SerializeEventsArray(const EventRecordArray& events, JSContext* cx,
+ JS::MutableHandle<JSObject*> result,
+ unsigned int dataset) {
+ // We serialize the events to a JS array.
+ JS::Rooted<JSObject*> eventsArray(cx,
+ JS::NewArrayObject(cx, events.Length()));
+ if (!eventsArray) {
+ return NS_ERROR_FAILURE;
+ }
+
+ for (uint32_t i = 0; i < events.Length(); ++i) {
+ const EventRecord& record = events[i];
+
+ // Each entry is an array of one of the forms:
+ // [timestamp, category, method, object, value]
+ // [timestamp, category, method, object, null, extra]
+ // [timestamp, category, method, object, value, extra]
+ JS::RootedVector<JS::Value> items(cx);
+
+ // Add timestamp.
+ JS::Rooted<JS::Value> val(cx);
+ if (!items.append(JS::NumberValue(floor(record.Timestamp())))) {
+ return NS_ERROR_FAILURE;
+ }
+
+ // Add category, method, object.
+ auto addCategoryMethodObjectValues = [&](const nsACString& category,
+ const nsACString& method,
+ const nsACString& object) -> bool {
+ return items.append(JS::StringValue(ToJSString(cx, category))) &&
+ items.append(JS::StringValue(ToJSString(cx, method))) &&
+ items.append(JS::StringValue(ToJSString(cx, object)));
+ };
+
+ const EventKey& eventKey = record.GetEventKey();
+ if (!eventKey.dynamic) {
+ const EventInfo& info = gEventInfo[eventKey.id];
+ if (!addCategoryMethodObjectValues(info.common_info.category(),
+ info.method(), info.object())) {
+ return NS_ERROR_FAILURE;
+ }
+ } else if (gDynamicEventInfo) {
+ const DynamicEventInfo& info = (*gDynamicEventInfo)[eventKey.id];
+ if (!addCategoryMethodObjectValues(info.category, info.method,
+ info.object)) {
+ return NS_ERROR_FAILURE;
+ }
+ }
+
+ // Add the optional string value only when needed.
+ // When the value field is empty and extra is not set, we can save a little
+ // space that way. We still need to submit a null value if extra is set, to
+ // match the form: [ts, category, method, object, null, extra]
+ if (record.Value()) {
+ if (!items.append(
+ JS::StringValue(ToJSString(cx, record.Value().value())))) {
+ return NS_ERROR_FAILURE;
+ }
+ } else if (!record.Extra().IsEmpty()) {
+ if (!items.append(JS::NullValue())) {
+ return NS_ERROR_FAILURE;
+ }
+ }
+
+ // Add the optional extra dictionary.
+ // To save a little space, only add it when it is not empty.
+ if (!record.Extra().IsEmpty()) {
+ JS::Rooted<JSObject*> obj(cx, JS_NewPlainObject(cx));
+ if (!obj) {
+ return NS_ERROR_FAILURE;
+ }
+
+ // Add extra key & value entries.
+ const ExtraArray& extra = record.Extra();
+ for (uint32_t i = 0; i < extra.Length(); ++i) {
+ JS::Rooted<JS::Value> value(cx);
+ value.setString(ToJSString(cx, extra[i].value));
+
+ if (!JS_DefineProperty(cx, obj, extra[i].key.get(), value,
+ JSPROP_ENUMERATE)) {
+ return NS_ERROR_FAILURE;
+ }
+ }
+ val.setObject(*obj);
+
+ if (!items.append(val)) {
+ return NS_ERROR_FAILURE;
+ }
+ }
+
+ // Add the record to the events array.
+ JS::Rooted<JSObject*> itemsArray(cx, JS::NewArrayObject(cx, items));
+ if (!JS_DefineElement(cx, eventsArray, i, itemsArray, JSPROP_ENUMERATE)) {
+ return NS_ERROR_FAILURE;
+ }
+ }
+
+ result.set(eventsArray);
+ return NS_OK;
+}
+
+} // anonymous namespace
+
+////////////////////////////////////////////////////////////////////////
+////////////////////////////////////////////////////////////////////////
+//
+// EXTERNALLY VISIBLE FUNCTIONS in namespace TelemetryEvents::
+
+// This is a StaticMutex rather than a plain Mutex (1) so that
+// it gets initialised in a thread-safe manner the first time
+// it is used, and (2) because it is never de-initialised, and
+// a normal Mutex would show up as a leak in BloatView. StaticMutex
+// also has the "OffTheBooks" property, so it won't show as a leak
+// in BloatView.
+// Another reason to use a StaticMutex instead of a plain Mutex is
+// that, due to the nature of Telemetry, we cannot rely on having a
+// mutex initialized in InitializeGlobalState. Unfortunately, we
+// cannot make sure that no other function is called before this point.
+static StaticMutex gTelemetryEventsMutex MOZ_UNANNOTATED;
+
+void TelemetryEvent::InitializeGlobalState(bool aCanRecordBase,
+ bool aCanRecordExtended) {
+ StaticMutexAutoLock locker(gTelemetryEventsMutex);
+ MOZ_ASSERT(!gInitDone,
+ "TelemetryEvent::InitializeGlobalState "
+ "may only be called once");
+
+ gCanRecordBase = aCanRecordBase;
+ gCanRecordExtended = aCanRecordExtended;
+
+ // Populate the static event name->id cache. Note that the event names are
+ // statically allocated and come from the automatically generated
+ // TelemetryEventData.h.
+ const uint32_t eventCount =
+ static_cast<uint32_t>(mozilla::Telemetry::EventID::EventCount);
+ for (uint32_t i = 0; i < eventCount; ++i) {
+ const EventInfo& info = gEventInfo[i];
+ uint32_t eventId = i;
+
+ // If this event is expired or not recorded in this process, mark it with
+ // a special event id.
+ // This avoids doing repeated checks at runtime.
+ if (IsExpiredVersion(info.common_info.expiration_version().get())) {
+ eventId = kExpiredEventId;
+ }
+
+ gEventNameIDMap.InsertOrUpdate(UniqueEventName(info),
+ EventKey{eventId, false});
+ gCategoryNames.Insert(info.common_info.category());
+ }
+
+ // A hack until bug 1691156 is fixed
+ gEnabledCategories.Insert("avif"_ns);
+
+ gInitDone = true;
+}
+
+void TelemetryEvent::DeInitializeGlobalState() {
+ StaticMutexAutoLock locker(gTelemetryEventsMutex);
+ MOZ_ASSERT(gInitDone);
+
+ gCanRecordBase = false;
+ gCanRecordExtended = false;
+
+ gEventNameIDMap.Clear();
+ gCategoryNames.Clear();
+ gEnabledCategories.Clear();
+ gEventRecords.Clear();
+
+ gDynamicEventInfo = nullptr;
+
+ gInitDone = false;
+}
+
+void TelemetryEvent::SetCanRecordBase(bool b) {
+ StaticMutexAutoLock locker(gTelemetryEventsMutex);
+ gCanRecordBase = b;
+}
+
+void TelemetryEvent::SetCanRecordExtended(bool b) {
+ StaticMutexAutoLock locker(gTelemetryEventsMutex);
+ gCanRecordExtended = b;
+}
+
+nsresult TelemetryEvent::RecordChildEvents(
+ ProcessID aProcessType,
+ const nsTArray<mozilla::Telemetry::ChildEventData>& aEvents) {
+ MOZ_ASSERT(XRE_IsParentProcess());
+ StaticMutexAutoLock locker(gTelemetryEventsMutex);
+ for (uint32_t i = 0; i < aEvents.Length(); ++i) {
+ const mozilla::Telemetry::ChildEventData& e = aEvents[i];
+
+ // Timestamps from child processes are absolute. We fix them up here to be
+ // relative to the main process start time.
+ // This allows us to put events from all processes on the same timeline.
+ double relativeTimestamp =
+ (e.timestamp - TimeStamp::ProcessCreation()).ToMilliseconds();
+
+ ::RecordEvent(locker, aProcessType, relativeTimestamp, e.category, e.method,
+ e.object, e.value, e.extra);
+ }
+ return NS_OK;
+}
+
+nsresult TelemetryEvent::RecordEvent(const nsACString& aCategory,
+ const nsACString& aMethod,
+ const nsACString& aObject,
+ JS::Handle<JS::Value> aValue,
+ JS::Handle<JS::Value> aExtra,
+ JSContext* cx, uint8_t optional_argc) {
+ // Check value argument.
+ if ((optional_argc > 0) && !aValue.isNull() && !aValue.isString()) {
+ LogToBrowserConsole(nsIScriptError::warningFlag,
+ u"Invalid type for value parameter."_ns);
+ mozilla::Telemetry::AccumulateCategorical(
+ LABELS_TELEMETRY_EVENT_RECORDING_ERROR::Value);
+ return NS_OK;
+ }
+
+ // Extract value parameter.
+ Maybe<nsCString> value;
+ if (aValue.isString()) {
+ nsAutoJSString jsStr;
+ if (!jsStr.init(cx, aValue)) {
+ LogToBrowserConsole(nsIScriptError::warningFlag,
+ u"Invalid string value for value parameter."_ns);
+ mozilla::Telemetry::AccumulateCategorical(
+ LABELS_TELEMETRY_EVENT_RECORDING_ERROR::Value);
+ return NS_OK;
+ }
+
+ nsCString str = NS_ConvertUTF16toUTF8(jsStr);
+ if (str.Length() > kMaxValueByteLength) {
+ LogToBrowserConsole(
+ nsIScriptError::warningFlag,
+ nsLiteralString(
+ u"Value parameter exceeds maximum string length, truncating."));
+ TruncateToByteLength(str, kMaxValueByteLength);
+ }
+ value = mozilla::Some(str);
+ }
+
+ // Check extra argument.
+ if ((optional_argc > 1) && !aExtra.isNull() && !aExtra.isObject()) {
+ LogToBrowserConsole(nsIScriptError::warningFlag,
+ u"Invalid type for extra parameter."_ns);
+ mozilla::Telemetry::AccumulateCategorical(
+ LABELS_TELEMETRY_EVENT_RECORDING_ERROR::Extra);
+ return NS_OK;
+ }
+
+ // Extract extra dictionary.
+ ExtraArray extra;
+ if (aExtra.isObject()) {
+ JS::Rooted<JSObject*> obj(cx, &aExtra.toObject());
+ JS::Rooted<JS::IdVector> ids(cx, JS::IdVector(cx));
+ if (!JS_Enumerate(cx, obj, &ids)) {
+ LogToBrowserConsole(nsIScriptError::warningFlag,
+ u"Failed to enumerate object."_ns);
+ mozilla::Telemetry::AccumulateCategorical(
+ LABELS_TELEMETRY_EVENT_RECORDING_ERROR::Extra);
+ return NS_OK;
+ }
+
+ for (size_t i = 0, n = ids.length(); i < n; i++) {
+ nsAutoJSString key;
+ if (!key.init(cx, ids[i])) {
+ LogToBrowserConsole(
+ nsIScriptError::warningFlag,
+ nsLiteralString(
+ u"Extra dictionary should only contain string keys."));
+ mozilla::Telemetry::AccumulateCategorical(
+ LABELS_TELEMETRY_EVENT_RECORDING_ERROR::Extra);
+ return NS_OK;
+ }
+
+ JS::Rooted<JS::Value> value(cx);
+ if (!JS_GetPropertyById(cx, obj, ids[i], &value)) {
+ LogToBrowserConsole(nsIScriptError::warningFlag,
+ u"Failed to get extra property."_ns);
+ mozilla::Telemetry::AccumulateCategorical(
+ LABELS_TELEMETRY_EVENT_RECORDING_ERROR::Extra);
+ return NS_OK;
+ }
+
+ nsAutoJSString jsStr;
+ if (!value.isString() || !jsStr.init(cx, value)) {
+ LogToBrowserConsole(nsIScriptError::warningFlag,
+ u"Extra properties should have string values."_ns);
+ mozilla::Telemetry::AccumulateCategorical(
+ LABELS_TELEMETRY_EVENT_RECORDING_ERROR::Extra);
+ return NS_OK;
+ }
+
+ nsCString str = NS_ConvertUTF16toUTF8(jsStr);
+ if (str.Length() > kMaxExtraValueByteLength) {
+ LogToBrowserConsole(
+ nsIScriptError::warningFlag,
+ nsLiteralString(
+ u"Extra value exceeds maximum string length, truncating."));
+ TruncateToByteLength(str, kMaxExtraValueByteLength);
+ }
+
+ extra.AppendElement(EventExtraEntry{NS_ConvertUTF16toUTF8(key), str});
+ }
+ }
+
+ // Lock for accessing internal data.
+ // While the lock is being held, no complex calls like JS calls can be made,
+ // as all of these could record Telemetry, which would result in deadlock.
+ RecordEventResult res;
+ if (!XRE_IsParentProcess()) {
+ {
+ StaticMutexAutoLock lock(gTelemetryEventsMutex);
+ res = ::ShouldRecordChildEvent(lock, aCategory, aMethod, aObject);
+ }
+
+ if (res == RecordEventResult::Ok) {
+ TelemetryIPCAccumulator::RecordChildEvent(
+ TimeStamp::NowLoRes(), aCategory, aMethod, aObject, value, extra);
+ }
+ } else {
+ StaticMutexAutoLock lock(gTelemetryEventsMutex);
+
+ if (!gInitDone) {
+ return NS_ERROR_FAILURE;
+ }
+
+ // Get the current time.
+ double timestamp = -1;
+ if (NS_WARN_IF(NS_FAILED(MsSinceProcessStart(&timestamp)))) {
+ return NS_ERROR_FAILURE;
+ }
+
+ res = ::RecordEvent(lock, ProcessID::Parent, timestamp, aCategory, aMethod,
+ aObject, value, extra);
+ }
+
+ // Trigger warnings or errors where needed.
+ switch (res) {
+ case RecordEventResult::UnknownEvent: {
+ nsPrintfCString msg(R"(Unknown event: ["%s", "%s", "%s"])",
+ PromiseFlatCString(aCategory).get(),
+ PromiseFlatCString(aMethod).get(),
+ PromiseFlatCString(aObject).get());
+ LogToBrowserConsole(nsIScriptError::errorFlag,
+ NS_ConvertUTF8toUTF16(msg));
+ return NS_OK;
+ }
+ case RecordEventResult::InvalidExtraKey: {
+ nsPrintfCString msg(R"(Invalid extra key for event ["%s", "%s", "%s"].)",
+ PromiseFlatCString(aCategory).get(),
+ PromiseFlatCString(aMethod).get(),
+ PromiseFlatCString(aObject).get());
+ LogToBrowserConsole(nsIScriptError::warningFlag,
+ NS_ConvertUTF8toUTF16(msg));
+ return NS_OK;
+ }
+ case RecordEventResult::StorageLimitReached: {
+ LogToBrowserConsole(nsIScriptError::warningFlag,
+ u"Event storage limit reached."_ns);
+ nsCOMPtr<nsIObserverService> serv =
+ mozilla::services::GetObserverService();
+ if (serv) {
+ serv->NotifyObservers(nullptr, "event-telemetry-storage-limit-reached",
+ nullptr);
+ }
+ return NS_OK;
+ }
+ default:
+ return NS_OK;
+ }
+}
+
+void TelemetryEvent::RecordEventNative(
+ mozilla::Telemetry::EventID aId, const mozilla::Maybe<nsCString>& aValue,
+ const mozilla::Maybe<ExtraArray>& aExtra) {
+ // Truncate aValue if present and necessary.
+ mozilla::Maybe<nsCString> value;
+ if (aValue) {
+ nsCString valueStr = aValue.ref();
+ if (valueStr.Length() > kMaxValueByteLength) {
+ TruncateToByteLength(valueStr, kMaxValueByteLength);
+ }
+ value = mozilla::Some(valueStr);
+ }
+
+ // Truncate any over-long extra values.
+ ExtraArray extra;
+ if (aExtra) {
+ extra = aExtra.value();
+ for (auto& item : extra) {
+ if (item.value.Length() > kMaxExtraValueByteLength) {
+ TruncateToByteLength(item.value, kMaxExtraValueByteLength);
+ }
+ }
+ }
+
+ const EventInfo& info = gEventInfo[static_cast<uint32_t>(aId)];
+ const nsCString category(info.common_info.category());
+ const nsCString method(info.method());
+ const nsCString object(info.object());
+ if (!XRE_IsParentProcess()) {
+ RecordEventResult res;
+ {
+ StaticMutexAutoLock lock(gTelemetryEventsMutex);
+ res = ::ShouldRecordChildEvent(lock, category, method, object);
+ }
+
+ if (res == RecordEventResult::Ok) {
+ TelemetryIPCAccumulator::RecordChildEvent(TimeStamp::NowLoRes(), category,
+ method, object, value, extra);
+ }
+ } else {
+ StaticMutexAutoLock lock(gTelemetryEventsMutex);
+
+ if (!gInitDone) {
+ return;
+ }
+
+ // Get the current time.
+ double timestamp = -1;
+ if (NS_WARN_IF(NS_FAILED(MsSinceProcessStart(&timestamp)))) {
+ return;
+ }
+
+ ::RecordEvent(lock, ProcessID::Parent, timestamp, category, method, object,
+ value, extra);
+ }
+}
+
+static bool GetArrayPropertyValues(JSContext* cx, JS::Handle<JSObject*> obj,
+ const char* property,
+ nsTArray<nsCString>* results) {
+ JS::Rooted<JS::Value> value(cx);
+ if (!JS_GetProperty(cx, obj, property, &value)) {
+ JS_ReportErrorASCII(cx, R"(Missing required property "%s" for event)",
+ property);
+ return false;
+ }
+
+ bool isArray = false;
+ if (!JS::IsArrayObject(cx, value, &isArray) || !isArray) {
+ JS_ReportErrorASCII(cx, R"(Property "%s" for event should be an array)",
+ property);
+ return false;
+ }
+
+ JS::Rooted<JSObject*> arrayObj(cx, &value.toObject());
+ uint32_t arrayLength;
+ if (!JS::GetArrayLength(cx, arrayObj, &arrayLength)) {
+ return false;
+ }
+
+ for (uint32_t arrayIdx = 0; arrayIdx < arrayLength; ++arrayIdx) {
+ JS::Rooted<JS::Value> element(cx);
+ if (!JS_GetElement(cx, arrayObj, arrayIdx, &element)) {
+ return false;
+ }
+
+ if (!element.isString()) {
+ JS_ReportErrorASCII(
+ cx, R"(Array entries for event property "%s" should be strings)",
+ property);
+ return false;
+ }
+
+ nsAutoJSString jsStr;
+ if (!jsStr.init(cx, element)) {
+ return false;
+ }
+
+ results->AppendElement(NS_ConvertUTF16toUTF8(jsStr));
+ }
+
+ return true;
+}
+
+nsresult TelemetryEvent::RegisterEvents(const nsACString& aCategory,
+ JS::Handle<JS::Value> aEventData,
+ bool aBuiltin, JSContext* cx) {
+ MOZ_ASSERT(XRE_IsParentProcess(),
+ "Events can only be registered in the parent process");
+
+ if (!IsValidIdentifierString(aCategory, 30, true, true)) {
+ JS_ReportErrorASCII(
+ cx, "Category parameter should match the identifier pattern.");
+ mozilla::Telemetry::AccumulateCategorical(
+ LABELS_TELEMETRY_EVENT_REGISTRATION_ERROR::Category);
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ if (!aEventData.isObject()) {
+ JS_ReportErrorASCII(cx, "Event data parameter should be an object");
+ mozilla::Telemetry::AccumulateCategorical(
+ LABELS_TELEMETRY_EVENT_REGISTRATION_ERROR::Other);
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ JS::Rooted<JSObject*> obj(cx, &aEventData.toObject());
+ JS::Rooted<JS::IdVector> eventPropertyIds(cx, JS::IdVector(cx));
+ if (!JS_Enumerate(cx, obj, &eventPropertyIds)) {
+ mozilla::Telemetry::AccumulateCategorical(
+ LABELS_TELEMETRY_EVENT_REGISTRATION_ERROR::Other);
+ return NS_ERROR_FAILURE;
+ }
+
+ // Collect the event data into local storage first.
+ // Only after successfully validating all contained events will we register
+ // them into global storage.
+ nsTArray<DynamicEventInfo> newEventInfos;
+ nsTArray<bool> newEventExpired;
+
+ for (size_t i = 0, n = eventPropertyIds.length(); i < n; i++) {
+ nsAutoJSString eventName;
+ if (!eventName.init(cx, eventPropertyIds[i])) {
+ mozilla::Telemetry::AccumulateCategorical(
+ LABELS_TELEMETRY_EVENT_REGISTRATION_ERROR::Other);
+ return NS_ERROR_FAILURE;
+ }
+
+ if (!IsValidIdentifierString(NS_ConvertUTF16toUTF8(eventName),
+ kMaxMethodNameByteLength, false, true)) {
+ JS_ReportErrorASCII(cx,
+ "Event names should match the identifier pattern.");
+ mozilla::Telemetry::AccumulateCategorical(
+ LABELS_TELEMETRY_EVENT_REGISTRATION_ERROR::Name);
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ JS::Rooted<JS::Value> value(cx);
+ if (!JS_GetPropertyById(cx, obj, eventPropertyIds[i], &value) ||
+ !value.isObject()) {
+ mozilla::Telemetry::AccumulateCategorical(
+ LABELS_TELEMETRY_EVENT_REGISTRATION_ERROR::Other);
+ return NS_ERROR_FAILURE;
+ }
+ JS::Rooted<JSObject*> eventObj(cx, &value.toObject());
+
+ // Extract the event registration data.
+ nsTArray<nsCString> methods;
+ nsTArray<nsCString> objects;
+ nsTArray<nsCString> extra_keys;
+ bool expired = false;
+ bool recordOnRelease = false;
+
+ // The methods & objects properties are required.
+ if (!GetArrayPropertyValues(cx, eventObj, "methods", &methods)) {
+ mozilla::Telemetry::AccumulateCategorical(
+ LABELS_TELEMETRY_EVENT_REGISTRATION_ERROR::Other);
+ return NS_ERROR_FAILURE;
+ }
+
+ if (!GetArrayPropertyValues(cx, eventObj, "objects", &objects)) {
+ mozilla::Telemetry::AccumulateCategorical(
+ LABELS_TELEMETRY_EVENT_REGISTRATION_ERROR::Other);
+ return NS_ERROR_FAILURE;
+ }
+
+ // extra_keys is optional.
+ bool hasProperty = false;
+ if (JS_HasProperty(cx, eventObj, "extra_keys", &hasProperty) &&
+ hasProperty) {
+ if (!GetArrayPropertyValues(cx, eventObj, "extra_keys", &extra_keys)) {
+ mozilla::Telemetry::AccumulateCategorical(
+ LABELS_TELEMETRY_EVENT_REGISTRATION_ERROR::Other);
+ return NS_ERROR_FAILURE;
+ }
+ }
+
+ // expired is optional.
+ if (JS_HasProperty(cx, eventObj, "expired", &hasProperty) && hasProperty) {
+ JS::Rooted<JS::Value> temp(cx);
+ if (!JS_GetProperty(cx, eventObj, "expired", &temp) ||
+ !temp.isBoolean()) {
+ mozilla::Telemetry::AccumulateCategorical(
+ LABELS_TELEMETRY_EVENT_REGISTRATION_ERROR::Other);
+ return NS_ERROR_FAILURE;
+ }
+
+ expired = temp.toBoolean();
+ }
+
+ // record_on_release is optional.
+ if (JS_HasProperty(cx, eventObj, "record_on_release", &hasProperty) &&
+ hasProperty) {
+ JS::Rooted<JS::Value> temp(cx);
+ if (!JS_GetProperty(cx, eventObj, "record_on_release", &temp) ||
+ !temp.isBoolean()) {
+ mozilla::Telemetry::AccumulateCategorical(
+ LABELS_TELEMETRY_EVENT_REGISTRATION_ERROR::Other);
+ return NS_ERROR_FAILURE;
+ }
+
+ recordOnRelease = temp.toBoolean();
+ }
+
+ // Validate methods.
+ for (auto& method : methods) {
+ if (!IsValidIdentifierString(method, kMaxMethodNameByteLength, false,
+ true)) {
+ JS_ReportErrorASCII(
+ cx, "Method names should match the identifier pattern.");
+ mozilla::Telemetry::AccumulateCategorical(
+ LABELS_TELEMETRY_EVENT_REGISTRATION_ERROR::Method);
+ return NS_ERROR_INVALID_ARG;
+ }
+ }
+
+ // Validate objects.
+ for (auto& object : objects) {
+ if (!IsValidIdentifierString(object, kMaxObjectNameByteLength, false,
+ true)) {
+ JS_ReportErrorASCII(
+ cx, "Object names should match the identifier pattern.");
+ mozilla::Telemetry::AccumulateCategorical(
+ LABELS_TELEMETRY_EVENT_REGISTRATION_ERROR::Object);
+ return NS_ERROR_INVALID_ARG;
+ }
+ }
+
+ // Validate extra keys.
+ if (extra_keys.Length() > kMaxExtraKeyCount) {
+ JS_ReportErrorASCII(cx, "No more than 10 extra keys can be registered.");
+ mozilla::Telemetry::AccumulateCategorical(
+ LABELS_TELEMETRY_EVENT_REGISTRATION_ERROR::ExtraKeys);
+ return NS_ERROR_INVALID_ARG;
+ }
+ for (auto& key : extra_keys) {
+ if (!IsValidIdentifierString(key, kMaxExtraKeyNameByteLength, false,
+ true)) {
+ JS_ReportErrorASCII(
+ cx, "Extra key names should match the identifier pattern.");
+ mozilla::Telemetry::AccumulateCategorical(
+ LABELS_TELEMETRY_EVENT_REGISTRATION_ERROR::ExtraKeys);
+ return NS_ERROR_INVALID_ARG;
+ }
+ }
+
+ // Append event infos to be registered.
+ for (auto& method : methods) {
+ for (auto& object : objects) {
+ // We defer the actual registration here in case any other event
+ // description is invalid. In that case we don't need to roll back any
+ // partial registration.
+ DynamicEventInfo info{aCategory, method, object,
+ extra_keys, recordOnRelease, aBuiltin};
+ newEventInfos.AppendElement(info);
+ newEventExpired.AppendElement(expired);
+ }
+ }
+ }
+
+ {
+ StaticMutexAutoLock locker(gTelemetryEventsMutex);
+ RegisterEvents(locker, aCategory, newEventInfos, newEventExpired, aBuiltin);
+ }
+
+ return NS_OK;
+}
+
+nsresult TelemetryEvent::CreateSnapshots(uint32_t aDataset, bool aClear,
+ uint32_t aEventLimit, JSContext* cx,
+ uint8_t optional_argc,
+ JS::MutableHandle<JS::Value> aResult) {
+ if (!XRE_IsParentProcess()) {
+ return NS_ERROR_FAILURE;
+ }
+
+ // Creating a JS snapshot of the events is a two-step process:
+ // (1) Lock the storage and copy the events into function-local storage.
+ // (2) Serialize the events into JS.
+ // We can't hold a lock for (2) because we will run into deadlocks otherwise
+ // from JS recording Telemetry.
+
+ // (1) Extract the events from storage with a lock held.
+ nsTArray<std::pair<const char*, EventRecordArray>> processEvents;
+ nsTArray<std::pair<uint32_t, EventRecordArray>> leftovers;
+ {
+ StaticMutexAutoLock locker(gTelemetryEventsMutex);
+
+ if (!gInitDone) {
+ return NS_ERROR_FAILURE;
+ }
+
+ // The snapshotting function is the same for both static and dynamic builtin
+ // events. We can use the same function and store the events in the same
+ // output storage.
+ auto snapshotter = [aDataset, &locker, &processEvents, &leftovers, aClear,
+ optional_argc,
+ aEventLimit](EventRecordsMapType& aProcessStorage) {
+ for (const auto& entry : aProcessStorage) {
+ const EventRecordArray* eventStorage = entry.GetWeak();
+ EventRecordArray events;
+ EventRecordArray leftoverEvents;
+
+ const uint32_t len = eventStorage->Length();
+ for (uint32_t i = 0; i < len; ++i) {
+ const EventRecord& record = (*eventStorage)[i];
+ if (IsInDataset(GetDataset(locker, record.GetEventKey()), aDataset)) {
+ // If we have a limit, adhere to it. If we have a limit and are
+ // going to clear, save the leftovers for later.
+ if (optional_argc < 2 || events.Length() < aEventLimit) {
+ events.AppendElement(record);
+ } else if (aClear) {
+ leftoverEvents.AppendElement(record);
+ }
+ }
+ }
+
+ if (events.Length()) {
+ const char* processName =
+ GetNameForProcessID(ProcessID(entry.GetKey()));
+ processEvents.EmplaceBack(processName, std::move(events));
+ if (leftoverEvents.Length()) {
+ leftovers.EmplaceBack(entry.GetKey(), std::move(leftoverEvents));
+ }
+ }
+ }
+ };
+
+ // Take a snapshot of the plain and dynamic builtin events.
+ snapshotter(gEventRecords);
+ if (aClear) {
+ gEventRecords.Clear();
+ for (auto& pair : leftovers) {
+ gEventRecords.InsertOrUpdate(
+ pair.first, MakeUnique<EventRecordArray>(std::move(pair.second)));
+ }
+ leftovers.Clear();
+ }
+ }
+
+ // (2) Serialize the events to a JS object.
+ JS::Rooted<JSObject*> rootObj(cx, JS_NewPlainObject(cx));
+ if (!rootObj) {
+ return NS_ERROR_FAILURE;
+ }
+
+ const uint32_t processLength = processEvents.Length();
+ for (uint32_t i = 0; i < processLength; ++i) {
+ JS::Rooted<JSObject*> eventsArray(cx);
+ if (NS_FAILED(SerializeEventsArray(processEvents[i].second, cx,
+ &eventsArray, aDataset))) {
+ return NS_ERROR_FAILURE;
+ }
+
+ if (!JS_DefineProperty(cx, rootObj, processEvents[i].first, eventsArray,
+ JSPROP_ENUMERATE)) {
+ return NS_ERROR_FAILURE;
+ }
+ }
+
+ aResult.setObject(*rootObj);
+ return NS_OK;
+}
+
+/**
+ * Resets all the stored events. This is intended to be only used in tests.
+ */
+void TelemetryEvent::ClearEvents() {
+ StaticMutexAutoLock lock(gTelemetryEventsMutex);
+
+ if (!gInitDone) {
+ return;
+ }
+
+ gEventRecords.Clear();
+}
+
+void TelemetryEvent::SetEventRecordingEnabled(const nsACString& category,
+ bool enabled) {
+ StaticMutexAutoLock locker(gTelemetryEventsMutex);
+
+ if (!gCategoryNames.Contains(category)) {
+ LogToBrowserConsole(
+ nsIScriptError::warningFlag,
+ NS_ConvertUTF8toUTF16(
+ nsLiteralCString(
+ "Unknown category for SetEventRecordingEnabled: ") +
+ category));
+ return;
+ }
+
+ if (enabled) {
+ gEnabledCategories.Insert(category);
+ } else {
+ gEnabledCategories.Remove(category);
+ }
+}
+
+size_t TelemetryEvent::SizeOfIncludingThis(
+ mozilla::MallocSizeOf aMallocSizeOf) {
+ StaticMutexAutoLock locker(gTelemetryEventsMutex);
+ size_t n = 0;
+
+ auto getSizeOfRecords = [aMallocSizeOf](auto& storageMap) {
+ size_t partial = storageMap.ShallowSizeOfExcludingThis(aMallocSizeOf);
+ for (const auto& eventRecords : storageMap.Values()) {
+ partial += eventRecords->ShallowSizeOfIncludingThis(aMallocSizeOf);
+
+ const uint32_t len = eventRecords->Length();
+ for (uint32_t i = 0; i < len; ++i) {
+ partial += (*eventRecords)[i].SizeOfExcludingThis(aMallocSizeOf);
+ }
+ }
+ return partial;
+ };
+
+ n += getSizeOfRecords(gEventRecords);
+
+ n += gEventNameIDMap.ShallowSizeOfExcludingThis(aMallocSizeOf);
+ for (auto iter = gEventNameIDMap.ConstIter(); !iter.Done(); iter.Next()) {
+ n += iter.Key().SizeOfExcludingThisIfUnshared(aMallocSizeOf);
+ }
+
+ n += gCategoryNames.ShallowSizeOfExcludingThis(aMallocSizeOf);
+ n += gEnabledCategories.ShallowSizeOfExcludingThis(aMallocSizeOf);
+
+ if (gDynamicEventInfo) {
+ n += gDynamicEventInfo->ShallowSizeOfIncludingThis(aMallocSizeOf);
+ for (auto& info : *gDynamicEventInfo) {
+ n += info.SizeOfExcludingThis(aMallocSizeOf);
+ }
+ }
+
+ return n;
+}
diff --git a/toolkit/components/telemetry/core/TelemetryEvent.h b/toolkit/components/telemetry/core/TelemetryEvent.h
new file mode 100644
index 0000000000..8734df1174
--- /dev/null
+++ b/toolkit/components/telemetry/core/TelemetryEvent.h
@@ -0,0 +1,71 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2; -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef TelemetryEvent_h__
+#define TelemetryEvent_h__
+
+#include <stdint.h>
+#include "js/TypeDecls.h"
+#include "mozilla/Maybe.h"
+#include "mozilla/TelemetryEventEnums.h"
+#include "mozilla/TelemetryProcessEnums.h"
+#include "nsTArray.h"
+#include "nsStringFwd.h"
+
+namespace mozilla {
+namespace Telemetry {
+struct ChildEventData;
+struct EventExtraEntry;
+} // namespace Telemetry
+} // namespace mozilla
+
+using mozilla::Telemetry::EventExtraEntry;
+
+// This module is internal to Telemetry. It encapsulates Telemetry's
+// event recording and storage logic. It should only be used by
+// Telemetry.cpp. These functions should not be used anywhere else.
+// For the public interface to Telemetry functionality, see Telemetry.h.
+
+namespace TelemetryEvent {
+
+void InitializeGlobalState(bool canRecordBase, bool canRecordExtended);
+void DeInitializeGlobalState();
+
+void SetCanRecordBase(bool b);
+void SetCanRecordExtended(bool b);
+
+// C++ API Endpoint.
+void RecordEventNative(
+ mozilla::Telemetry::EventID aId, const mozilla::Maybe<nsCString>& aValue,
+ const mozilla::Maybe<CopyableTArray<EventExtraEntry>>& aExtra);
+
+// JS API Endpoints.
+nsresult RecordEvent(const nsACString& aCategory, const nsACString& aMethod,
+ const nsACString& aObject, JS::Handle<JS::Value> aValue,
+ JS::Handle<JS::Value> aExtra, JSContext* aCx,
+ uint8_t optional_argc);
+
+void SetEventRecordingEnabled(const nsACString& aCategory, bool aEnabled);
+nsresult RegisterEvents(const nsACString& aCategory,
+ JS::Handle<JS::Value> aEventData, bool aBuiltin,
+ JSContext* cx);
+
+nsresult CreateSnapshots(uint32_t aDataset, bool aClear, uint32_t aEventLimit,
+ JSContext* aCx, uint8_t optional_argc,
+ JS::MutableHandle<JS::Value> aResult);
+
+// Record events from child processes.
+nsresult RecordChildEvents(
+ mozilla::Telemetry::ProcessID aProcessType,
+ const nsTArray<mozilla::Telemetry::ChildEventData>& aEvents);
+
+// Only to be used for testing.
+void ClearEvents();
+
+size_t SizeOfIncludingThis(mozilla::MallocSizeOf aMallocSizeOf);
+
+} // namespace TelemetryEvent
+
+#endif // TelemetryEvent_h__
diff --git a/toolkit/components/telemetry/core/TelemetryHistogram.cpp b/toolkit/components/telemetry/core/TelemetryHistogram.cpp
new file mode 100644
index 0000000000..88ac88eb9e
--- /dev/null
+++ b/toolkit/components/telemetry/core/TelemetryHistogram.cpp
@@ -0,0 +1,3677 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "TelemetryHistogram.h"
+
+#include <limits>
+#include "base/histogram.h"
+#include "geckoview/streaming/GeckoViewStreamingTelemetry.h"
+#include "ipc/TelemetryIPCAccumulator.h"
+#include "jsapi.h"
+#include "jsfriendapi.h"
+#include "js/Array.h" // JS::GetArrayLength, JS::IsArrayObject, JS::NewArrayObject
+#include "js/GCAPI.h"
+#include "js/Object.h" // JS::GetClass, JS::GetMaybePtrFromReservedSlot, JS::SetReservedSlot
+#include "js/PropertyAndElement.h" // JS_DefineElement, JS_DefineFunction, JS_DefineProperty, JS_DefineUCProperty, JS_Enumerate, JS_GetElement, JS_GetProperty, JS_GetPropertyById
+#include "mozilla/dom/ToJSValue.h"
+#include "mozilla/gfx/GPUProcessManager.h"
+#include "mozilla/Atomics.h"
+#include "mozilla/JSONWriter.h"
+#include "mozilla/StartupTimeline.h"
+#include "mozilla/StaticMutex.h"
+#include "mozilla/Unused.h"
+#include "nsClassHashtable.h"
+#include "nsString.h"
+#include "nsHashKeys.h"
+#include "nsITelemetry.h"
+#include "nsPrintfCString.h"
+#include "TelemetryHistogramNameMap.h"
+#include "TelemetryScalar.h"
+
+using base::BooleanHistogram;
+using base::CountHistogram;
+using base::FlagHistogram;
+using base::LinearHistogram;
+using mozilla::MakeUnique;
+using mozilla::StaticMutex;
+using mozilla::StaticMutexAutoLock;
+using mozilla::UniquePtr;
+using mozilla::Telemetry::HistogramAccumulation;
+using mozilla::Telemetry::HistogramCount;
+using mozilla::Telemetry::HistogramID;
+using mozilla::Telemetry::HistogramIDByNameLookup;
+using mozilla::Telemetry::KeyedHistogramAccumulation;
+using mozilla::Telemetry::ProcessID;
+using mozilla::Telemetry::Common::CanRecordDataset;
+using mozilla::Telemetry::Common::CanRecordProduct;
+using mozilla::Telemetry::Common::GetCurrentProduct;
+using mozilla::Telemetry::Common::GetIDForProcessName;
+using mozilla::Telemetry::Common::GetNameForProcessID;
+using mozilla::Telemetry::Common::IsExpiredVersion;
+using mozilla::Telemetry::Common::IsInDataset;
+using mozilla::Telemetry::Common::LogToBrowserConsole;
+using mozilla::Telemetry::Common::RecordedProcessType;
+using mozilla::Telemetry::Common::StringHashSet;
+using mozilla::Telemetry::Common::SupportedProduct;
+using mozilla::Telemetry::Common::ToJSString;
+
+namespace TelemetryIPCAccumulator = mozilla::TelemetryIPCAccumulator;
+
+////////////////////////////////////////////////////////////////////////
+////////////////////////////////////////////////////////////////////////
+//
+// Naming: there are two kinds of functions in this file:
+//
+// * Functions named internal_*: these can only be reached via an
+// interface function (TelemetryHistogram::*). They mostly expect
+// the interface function to have acquired
+// |gTelemetryHistogramMutex|, so they do not have to be
+// thread-safe. However, those internal_* functions that are
+// reachable from internal_WrapAndReturnHistogram and
+// internal_WrapAndReturnKeyedHistogram can sometimes be called
+// without |gTelemetryHistogramMutex|, and so might be racey.
+//
+// * Functions named TelemetryHistogram::*. This is the external interface.
+// Entries and exits to these functions are serialised using
+// |gTelemetryHistogramMutex|, except for GetKeyedHistogramSnapshots and
+// CreateHistogramSnapshots.
+//
+// Avoiding races and deadlocks:
+//
+// All functions in the external interface (TelemetryHistogram::*) are
+// serialised using the mutex |gTelemetryHistogramMutex|. This means
+// that the external interface is thread-safe, and many of the
+// internal_* functions can ignore thread safety. But it also brings
+// a danger of deadlock if any function in the external interface can
+// get back to that interface. That is, we will deadlock on any call
+// chain like this
+//
+// TelemetryHistogram::* -> .. any functions .. -> TelemetryHistogram::*
+//
+// To reduce the danger of that happening, observe the following rules:
+//
+// * No function in TelemetryHistogram::* may directly call, nor take the
+// address of, any other function in TelemetryHistogram::*.
+//
+// * No internal function internal_* may call, nor take the address
+// of, any function in TelemetryHistogram::*.
+//
+// internal_WrapAndReturnHistogram and
+// internal_WrapAndReturnKeyedHistogram are not protected by
+// |gTelemetryHistogramMutex| because they make calls to the JS
+// engine, but that can in turn call back to Telemetry and hence back
+// to a TelemetryHistogram:: function, in order to report GC and other
+// statistics. This would lead to deadlock due to attempted double
+// acquisition of |gTelemetryHistogramMutex|, if the internal_* functions
+// were required to be protected by |gTelemetryHistogramMutex|. To
+// break that cycle, we relax that requirement. Unfortunately this
+// means that this file is not guaranteed race-free.
+
+// This is a StaticMutex rather than a plain Mutex (1) so that
+// it gets initialised in a thread-safe manner the first time
+// it is used, and (2) because it is never de-initialised, and
+// a normal Mutex would show up as a leak in BloatView. StaticMutex
+// also has the "OffTheBooks" property, so it won't show as a leak
+// in BloatView.
+static StaticMutex gTelemetryHistogramMutex MOZ_UNANNOTATED;
+
+////////////////////////////////////////////////////////////////////////
+////////////////////////////////////////////////////////////////////////
+//
+// PRIVATE TYPES
+
+namespace {
+
+// Hardcoded probes
+//
+// The order of elements here is important to minimize the memory footprint of a
+// HistogramInfo instance.
+//
+// Any adjustements need to be reflected in gen_histogram_data.py
+struct HistogramInfo {
+ uint32_t min;
+ uint32_t max;
+ uint32_t bucketCount;
+ uint32_t name_offset;
+ uint32_t expiration_offset;
+ uint32_t label_count;
+ uint32_t key_count;
+ uint32_t store_count;
+ uint16_t label_index;
+ uint16_t key_index;
+ uint16_t store_index;
+ RecordedProcessType record_in_processes;
+ bool keyed;
+ uint8_t histogramType;
+ uint8_t dataset;
+ SupportedProduct products;
+
+ const char* name() const;
+ const char* expiration() const;
+ nsresult label_id(const char* label, uint32_t* labelId) const;
+ bool allows_key(const nsACString& key) const;
+ bool is_single_store() const;
+};
+
+// Structs used to keep information about the histograms for which a
+// snapshot should be created.
+struct HistogramSnapshotData {
+ CopyableTArray<base::Histogram::Sample> mBucketRanges;
+ CopyableTArray<base::Histogram::Count> mBucketCounts;
+ int64_t mSampleSum; // Same type as base::Histogram::SampleSet::sum_
+};
+
+struct HistogramSnapshotInfo {
+ HistogramSnapshotData data;
+ HistogramID histogramID;
+};
+
+typedef mozilla::Vector<HistogramSnapshotInfo> HistogramSnapshotsArray;
+typedef mozilla::Vector<HistogramSnapshotsArray> HistogramProcessSnapshotsArray;
+
+// The following is used to handle snapshot information for keyed histograms.
+typedef nsTHashMap<nsCStringHashKey, HistogramSnapshotData>
+ KeyedHistogramSnapshotData;
+
+struct KeyedHistogramSnapshotInfo {
+ KeyedHistogramSnapshotData data;
+ HistogramID histogramId;
+};
+
+typedef mozilla::Vector<KeyedHistogramSnapshotInfo>
+ KeyedHistogramSnapshotsArray;
+typedef mozilla::Vector<KeyedHistogramSnapshotsArray>
+ KeyedHistogramProcessSnapshotsArray;
+
+/**
+ * A Histogram storage engine.
+ *
+ * Takes care of recording data into multiple stores if necessary.
+ */
+class Histogram {
+ public:
+ /*
+ * Create a new histogram instance from the given info.
+ *
+ * If the histogram is already expired, this does not allocate.
+ */
+ Histogram(HistogramID histogramId, const HistogramInfo& info, bool expired);
+ ~Histogram();
+
+ /**
+ * Add a sample to this histogram in all registered stores.
+ */
+ void Add(uint32_t sample);
+
+ /**
+ * Clear the named store for this histogram.
+ */
+ void Clear(const nsACString& store);
+
+ /**
+ * Get the histogram instance from the named store.
+ */
+ bool GetHistogram(const nsACString& store, base::Histogram** h);
+
+ bool IsExpired() const { return mIsExpired; }
+
+ size_t SizeOfIncludingThis(mozilla::MallocSizeOf aMallocSizeOf);
+
+ private:
+ // String -> Histogram*
+ typedef nsClassHashtable<nsCStringHashKey, base::Histogram> HistogramStore;
+ HistogramStore mStorage;
+
+ // A valid pointer if this histogram belongs to only the main store
+ base::Histogram* mSingleStore;
+
+ // We don't track stores for expired histograms.
+ // We just store a single flag and all other operations become a no-op.
+ bool mIsExpired;
+};
+
+class KeyedHistogram {
+ public:
+ KeyedHistogram(HistogramID id, const HistogramInfo& info, bool expired);
+ ~KeyedHistogram();
+ nsresult GetHistogram(const nsCString& aStore, const nsCString& key,
+ base::Histogram** histogram);
+ base::Histogram* GetHistogram(const nsCString& aStore, const nsCString& key);
+ uint32_t GetHistogramType() const { return mHistogramInfo.histogramType; }
+ nsresult GetKeys(const StaticMutexAutoLock& aLock, const nsCString& store,
+ nsTArray<nsCString>& aKeys);
+ // Note: unlike other methods, GetJSSnapshot is thread safe.
+ nsresult GetJSSnapshot(JSContext* cx, JS::Handle<JSObject*> obj,
+ const nsACString& aStore, bool clearSubsession);
+ nsresult GetSnapshot(const StaticMutexAutoLock& aLock,
+ const nsACString& aStore,
+ KeyedHistogramSnapshotData& aSnapshot,
+ bool aClearSubsession);
+
+ nsresult Add(const nsCString& key, uint32_t aSample, ProcessID aProcessType);
+ void Clear(const nsACString& aStore);
+
+ HistogramID GetHistogramID() const { return mId; }
+
+ bool IsEmpty(const nsACString& aStore) const;
+
+ bool IsExpired() const { return mIsExpired; }
+
+ size_t SizeOfIncludingThis(mozilla::MallocSizeOf aMallocSizeOf);
+
+ private:
+ typedef nsClassHashtable<nsCStringHashKey, base::Histogram>
+ KeyedHistogramMapType;
+ typedef nsClassHashtable<nsCStringHashKey, KeyedHistogramMapType>
+ StoreMapType;
+
+ StoreMapType mStorage;
+ // A valid map if this histogram belongs to only the main store
+ KeyedHistogramMapType* mSingleStore;
+
+ const HistogramID mId;
+ const HistogramInfo& mHistogramInfo;
+ bool mIsExpired;
+};
+
+} // namespace
+
+////////////////////////////////////////////////////////////////////////
+////////////////////////////////////////////////////////////////////////
+//
+// PRIVATE STATE, SHARED BY ALL THREADS
+
+namespace {
+
+// Set to true once this global state has been initialized
+bool gInitDone = false;
+
+// Whether we are collecting the base, opt-out, Histogram data.
+bool gCanRecordBase = false;
+// Whether we are collecting the extended, opt-in, Histogram data.
+bool gCanRecordExtended = false;
+
+// The storage for actual Histogram instances.
+// We use separate ones for plain and keyed histograms.
+Histogram** gHistogramStorage;
+// Keyed histograms internally map string keys to individual Histogram
+// instances.
+KeyedHistogram** gKeyedHistogramStorage;
+
+// To simplify logic below we use a single histogram instance for all expired
+// histograms.
+Histogram* gExpiredHistogram = nullptr;
+
+// The single placeholder for expired keyed histograms.
+KeyedHistogram* gExpiredKeyedHistogram = nullptr;
+
+// This tracks whether recording is enabled for specific histograms.
+// To utilize C++ initialization rules, we invert the meaning to "disabled".
+bool gHistogramRecordingDisabled[HistogramCount] = {};
+
+// This is for gHistogramInfos, gHistogramStringTable
+#include "TelemetryHistogramData.inc"
+
+} // namespace
+
+////////////////////////////////////////////////////////////////////////
+////////////////////////////////////////////////////////////////////////
+//
+// PRIVATE CONSTANTS
+
+namespace {
+
+// List of histogram IDs which should have recording disabled initially.
+const HistogramID kRecordingInitiallyDisabledIDs[] = {
+ mozilla::Telemetry::FX_REFRESH_DRIVER_SYNC_SCROLL_FRAME_DELAY_MS,
+
+ // The array must not be empty. Leave these item here.
+ mozilla::Telemetry::TELEMETRY_TEST_COUNT_INIT_NO_RECORD,
+ mozilla::Telemetry::TELEMETRY_TEST_KEYED_COUNT_INIT_NO_RECORD};
+
+const char* TEST_HISTOGRAM_PREFIX = "TELEMETRY_TEST_";
+
+} // namespace
+
+////////////////////////////////////////////////////////////////////////
+////////////////////////////////////////////////////////////////////////
+//
+// The core storage access functions.
+// They wrap access to the histogram storage and lookup caches.
+
+namespace {
+
+size_t internal_KeyedHistogramStorageIndex(HistogramID aHistogramId,
+ ProcessID aProcessId) {
+ return aHistogramId * size_t(ProcessID::Count) + size_t(aProcessId);
+}
+
+size_t internal_HistogramStorageIndex(const StaticMutexAutoLock& aLock,
+ HistogramID aHistogramId,
+ ProcessID aProcessId) {
+ static_assert(HistogramCount < std::numeric_limits<size_t>::max() /
+ size_t(ProcessID::Count),
+ "Too many histograms and processes to store in a 1D array.");
+
+ return aHistogramId * size_t(ProcessID::Count) + size_t(aProcessId);
+}
+
+Histogram* internal_GetHistogramFromStorage(const StaticMutexAutoLock& aLock,
+ HistogramID aHistogramId,
+ ProcessID aProcessId) {
+ size_t index =
+ internal_HistogramStorageIndex(aLock, aHistogramId, aProcessId);
+ return gHistogramStorage[index];
+}
+
+void internal_SetHistogramInStorage(const StaticMutexAutoLock& aLock,
+ HistogramID aHistogramId,
+ ProcessID aProcessId,
+ Histogram* aHistogram) {
+ MOZ_ASSERT(XRE_IsParentProcess(),
+ "Histograms are stored only in the parent process.");
+
+ size_t index =
+ internal_HistogramStorageIndex(aLock, aHistogramId, aProcessId);
+ MOZ_ASSERT(!gHistogramStorage[index],
+ "Mustn't overwrite storage without clearing it first.");
+ gHistogramStorage[index] = aHistogram;
+}
+
+KeyedHistogram* internal_GetKeyedHistogramFromStorage(HistogramID aHistogramId,
+ ProcessID aProcessId) {
+ size_t index = internal_KeyedHistogramStorageIndex(aHistogramId, aProcessId);
+ return gKeyedHistogramStorage[index];
+}
+
+void internal_SetKeyedHistogramInStorage(HistogramID aHistogramId,
+ ProcessID aProcessId,
+ KeyedHistogram* aKeyedHistogram) {
+ MOZ_ASSERT(XRE_IsParentProcess(),
+ "Keyed Histograms are stored only in the parent process.");
+
+ size_t index = internal_KeyedHistogramStorageIndex(aHistogramId, aProcessId);
+ MOZ_ASSERT(!gKeyedHistogramStorage[index],
+ "Mustn't overwrite storage without clearing it first");
+ gKeyedHistogramStorage[index] = aKeyedHistogram;
+}
+
+// Factory function for base::Histogram instances.
+base::Histogram* internal_CreateBaseHistogramInstance(const HistogramInfo& info,
+ int bucketsOffset);
+
+// Factory function for histogram instances.
+Histogram* internal_CreateHistogramInstance(HistogramID histogramId);
+
+bool internal_IsHistogramEnumId(HistogramID aID) {
+ static_assert(((HistogramID)-1 > 0), "ID should be unsigned.");
+ return aID < HistogramCount;
+}
+
+// Look up a plain histogram by id.
+Histogram* internal_GetHistogramById(const StaticMutexAutoLock& aLock,
+ HistogramID histogramId,
+ ProcessID processId,
+ bool instantiate = true) {
+ MOZ_ASSERT(internal_IsHistogramEnumId(histogramId));
+ MOZ_ASSERT(!gHistogramInfos[histogramId].keyed);
+ MOZ_ASSERT(processId < ProcessID::Count);
+
+ Histogram* h =
+ internal_GetHistogramFromStorage(aLock, histogramId, processId);
+ if (h || !instantiate) {
+ return h;
+ }
+
+ h = internal_CreateHistogramInstance(histogramId);
+ MOZ_ASSERT(h);
+ internal_SetHistogramInStorage(aLock, histogramId, processId, h);
+
+ return h;
+}
+
+// Look up a keyed histogram by id.
+KeyedHistogram* internal_GetKeyedHistogramById(HistogramID histogramId,
+ ProcessID processId,
+ bool instantiate = true) {
+ MOZ_ASSERT(internal_IsHistogramEnumId(histogramId));
+ MOZ_ASSERT(gHistogramInfos[histogramId].keyed);
+ MOZ_ASSERT(processId < ProcessID::Count);
+
+ KeyedHistogram* kh =
+ internal_GetKeyedHistogramFromStorage(histogramId, processId);
+ if (kh || !instantiate) {
+ return kh;
+ }
+
+ const HistogramInfo& info = gHistogramInfos[histogramId];
+ const bool isExpired = IsExpiredVersion(info.expiration());
+
+ // If the keyed histogram is expired, set its storage to the expired
+ // keyed histogram.
+ if (isExpired) {
+ if (!gExpiredKeyedHistogram) {
+ // If we don't have an expired keyed histogram, create one.
+ gExpiredKeyedHistogram =
+ new KeyedHistogram(histogramId, info, true /* expired */);
+ MOZ_ASSERT(gExpiredKeyedHistogram);
+ }
+ kh = gExpiredKeyedHistogram;
+ } else {
+ kh = new KeyedHistogram(histogramId, info, false /* expired */);
+ }
+
+ internal_SetKeyedHistogramInStorage(histogramId, processId, kh);
+
+ return kh;
+}
+
+// Look up a histogram id from a histogram name.
+nsresult internal_GetHistogramIdByName(const StaticMutexAutoLock& aLock,
+ const nsACString& name,
+ HistogramID* id) {
+ const uint32_t idx = HistogramIDByNameLookup(name);
+ MOZ_ASSERT(idx < HistogramCount,
+ "Intermediate lookup should always give a valid index.");
+
+ // The lookup hashes the input and uses it as an index into the value array.
+ // Hash collisions can still happen for unknown values,
+ // therefore we check that the name matches.
+ if (name.Equals(gHistogramInfos[idx].name())) {
+ *id = HistogramID(idx);
+ return NS_OK;
+ }
+
+ return NS_ERROR_ILLEGAL_VALUE;
+}
+
+} // namespace
+
+////////////////////////////////////////////////////////////////////////
+////////////////////////////////////////////////////////////////////////
+//
+// PRIVATE: Misc small helpers
+
+namespace {
+
+bool internal_CanRecordBase() { return gCanRecordBase; }
+
+bool internal_CanRecordExtended() { return gCanRecordExtended; }
+
+bool internal_AttemptedGPUProcess() {
+ // Check if it was tried to launch a process.
+ bool attemptedGPUProcess = false;
+ if (auto gpm = mozilla::gfx::GPUProcessManager::Get()) {
+ attemptedGPUProcess = gpm->AttemptedGPUProcess();
+ }
+ return attemptedGPUProcess;
+}
+
+// Note: this is completely unrelated to mozilla::IsEmpty.
+bool internal_IsEmpty(const StaticMutexAutoLock& aLock,
+ const base::Histogram* h) {
+ return h->is_empty();
+}
+
+void internal_SetHistogramRecordingEnabled(const StaticMutexAutoLock& aLock,
+ HistogramID id, bool aEnabled) {
+ MOZ_ASSERT(internal_IsHistogramEnumId(id));
+ gHistogramRecordingDisabled[id] = !aEnabled;
+}
+
+bool internal_IsRecordingEnabled(HistogramID id) {
+ MOZ_ASSERT(internal_IsHistogramEnumId(id));
+ return !gHistogramRecordingDisabled[id];
+}
+
+const char* HistogramInfo::name() const {
+ return &gHistogramStringTable[this->name_offset];
+}
+
+const char* HistogramInfo::expiration() const {
+ return &gHistogramStringTable[this->expiration_offset];
+}
+
+nsresult HistogramInfo::label_id(const char* label, uint32_t* labelId) const {
+ MOZ_ASSERT(label);
+ MOZ_ASSERT(this->histogramType == nsITelemetry::HISTOGRAM_CATEGORICAL);
+ if (this->histogramType != nsITelemetry::HISTOGRAM_CATEGORICAL) {
+ return NS_ERROR_FAILURE;
+ }
+
+ for (uint32_t i = 0; i < this->label_count; ++i) {
+ // gHistogramLabelTable contains the indices of the label strings in the
+ // gHistogramStringTable.
+ // They are stored in-order and consecutively, from the offset label_index
+ // to (label_index + label_count).
+ uint32_t string_offset = gHistogramLabelTable[this->label_index + i];
+ const char* const str = &gHistogramStringTable[string_offset];
+ if (::strcmp(label, str) == 0) {
+ *labelId = i;
+ return NS_OK;
+ }
+ }
+
+ return NS_ERROR_FAILURE;
+}
+
+bool HistogramInfo::allows_key(const nsACString& key) const {
+ MOZ_ASSERT(this->keyed);
+
+ // If we didn't specify a list of allowed keys, just return true.
+ if (this->key_count == 0) {
+ return true;
+ }
+
+ // Otherwise, check if |key| is in the list of allowed keys.
+ for (uint32_t i = 0; i < this->key_count; ++i) {
+ // gHistogramKeyTable contains the indices of the key strings in the
+ // gHistogramStringTable. They are stored in-order and consecutively,
+ // from the offset key_index to (key_index + key_count).
+ uint32_t string_offset = gHistogramKeyTable[this->key_index + i];
+ const char* const str = &gHistogramStringTable[string_offset];
+ if (key.EqualsASCII(str)) {
+ return true;
+ }
+ }
+
+ // |key| was not found.
+ return false;
+}
+
+bool HistogramInfo::is_single_store() const {
+ return store_count == 1 && store_index == UINT16_MAX;
+}
+
+} // namespace
+
+////////////////////////////////////////////////////////////////////////
+////////////////////////////////////////////////////////////////////////
+//
+// PRIVATE: Histogram Get, Add, Clone, Clear functions
+
+namespace {
+
+nsresult internal_CheckHistogramArguments(const HistogramInfo& info) {
+ if (info.histogramType != nsITelemetry::HISTOGRAM_BOOLEAN &&
+ info.histogramType != nsITelemetry::HISTOGRAM_FLAG &&
+ info.histogramType != nsITelemetry::HISTOGRAM_COUNT) {
+ // Sanity checks for histogram parameters.
+ if (info.min >= info.max) {
+ return NS_ERROR_ILLEGAL_VALUE;
+ }
+
+ if (info.bucketCount <= 2) {
+ return NS_ERROR_ILLEGAL_VALUE;
+ }
+
+ if (info.min < 1) {
+ return NS_ERROR_ILLEGAL_VALUE;
+ }
+ }
+
+ return NS_OK;
+}
+
+Histogram* internal_CreateHistogramInstance(HistogramID histogramId) {
+ const HistogramInfo& info = gHistogramInfos[histogramId];
+
+ if (NS_FAILED(internal_CheckHistogramArguments(info))) {
+ MOZ_ASSERT(false, "Failed histogram argument checks.");
+ return nullptr;
+ }
+
+ const bool isExpired = IsExpiredVersion(info.expiration());
+
+ if (isExpired) {
+ if (!gExpiredHistogram) {
+ gExpiredHistogram = new Histogram(histogramId, info, /* expired */ true);
+ }
+
+ return gExpiredHistogram;
+ }
+
+ Histogram* wrapper = new Histogram(histogramId, info, /* expired */ false);
+
+ return wrapper;
+}
+
+base::Histogram* internal_CreateBaseHistogramInstance(
+ const HistogramInfo& passedInfo, int bucketsOffset) {
+ if (NS_FAILED(internal_CheckHistogramArguments(passedInfo))) {
+ MOZ_ASSERT(false, "Failed histogram argument checks.");
+ return nullptr;
+ }
+
+ // We don't actually store data for expired histograms at all.
+ MOZ_ASSERT(!IsExpiredVersion(passedInfo.expiration()));
+
+ HistogramInfo info = passedInfo;
+ const int* buckets = &gHistogramBucketLowerBounds[bucketsOffset];
+
+ base::Histogram::Flags flags = base::Histogram::kNoFlags;
+ base::Histogram* h = nullptr;
+ switch (info.histogramType) {
+ case nsITelemetry::HISTOGRAM_EXPONENTIAL:
+ h = base::Histogram::FactoryGet(info.min, info.max, info.bucketCount,
+ flags, buckets);
+ break;
+ case nsITelemetry::HISTOGRAM_LINEAR:
+ case nsITelemetry::HISTOGRAM_CATEGORICAL:
+ h = LinearHistogram::FactoryGet(info.min, info.max, info.bucketCount,
+ flags, buckets);
+ break;
+ case nsITelemetry::HISTOGRAM_BOOLEAN:
+ h = BooleanHistogram::FactoryGet(flags, buckets);
+ break;
+ case nsITelemetry::HISTOGRAM_FLAG:
+ h = FlagHistogram::FactoryGet(flags, buckets);
+ break;
+ case nsITelemetry::HISTOGRAM_COUNT:
+ h = CountHistogram::FactoryGet(flags, buckets);
+ break;
+ default:
+ MOZ_ASSERT(false, "Invalid histogram type");
+ return nullptr;
+ }
+
+ return h;
+}
+
+nsresult internal_HistogramAdd(const StaticMutexAutoLock& aLock,
+ Histogram& histogram, const HistogramID id,
+ uint32_t value, ProcessID aProcessType) {
+ // Check if we are allowed to record the data.
+ bool canRecordDataset =
+ CanRecordDataset(gHistogramInfos[id].dataset, internal_CanRecordBase(),
+ internal_CanRecordExtended());
+ // If `histogram` is a non-parent-process histogram, then recording-enabled
+ // has been checked in its owner process.
+ if (!canRecordDataset ||
+ (aProcessType == ProcessID::Parent && !internal_IsRecordingEnabled(id))) {
+ return NS_OK;
+ }
+
+ // Don't record if the current platform is not enabled
+ if (!CanRecordProduct(gHistogramInfos[id].products)) {
+ return NS_OK;
+ }
+
+ if (&histogram != gExpiredHistogram &&
+ GetCurrentProduct() == SupportedProduct::GeckoviewStreaming) {
+ const HistogramInfo& info = gHistogramInfos[id];
+ GeckoViewStreamingTelemetry::HistogramAccumulate(
+ nsDependentCString(info.name()),
+ info.histogramType == nsITelemetry::HISTOGRAM_CATEGORICAL, value);
+ return NS_OK;
+ }
+
+ // The internal representation of a base::Histogram's buckets uses `int`.
+ // Clamp large values of `value` to be INT_MAX so they continue to be treated
+ // as large values (instead of negative ones).
+ if (value > INT_MAX) {
+ TelemetryScalar::Add(
+ mozilla::Telemetry::ScalarID::TELEMETRY_ACCUMULATE_CLAMPED_VALUES,
+ NS_ConvertASCIItoUTF16(gHistogramInfos[id].name()), 1);
+ value = INT_MAX;
+ }
+
+ histogram.Add(value);
+
+ return NS_OK;
+}
+
+} // namespace
+
+////////////////////////////////////////////////////////////////////////
+////////////////////////////////////////////////////////////////////////
+//
+// PRIVATE: Histogram reflection helpers
+
+namespace {
+
+/**
+ * Copy histograms and samples to Mozilla-friendly structures.
+ * Please note that this version does not make use of JS contexts.
+ *
+ * @param {StaticMutexAutoLock} the proof we hold the mutex.
+ * @param {Histogram} the histogram to reflect.
+ * @return {nsresult} NS_ERROR_FAILURE if we fail to allocate memory for the
+ * snapshot.
+ */
+nsresult internal_GetHistogramAndSamples(const StaticMutexAutoLock& aLock,
+ const base::Histogram* h,
+ HistogramSnapshotData& aSnapshot) {
+ MOZ_ASSERT(h);
+
+ // Convert the ranges of the buckets to a nsTArray.
+ const size_t bucketCount = h->bucket_count();
+ for (size_t i = 0; i < bucketCount; i++) {
+ // XXX(Bug 1631371) Check if this should use a fallible operation as it
+ // pretended earlier, or change the return type to void.
+ aSnapshot.mBucketRanges.AppendElement(h->ranges(i));
+ }
+
+ // Get a snapshot of the samples.
+ base::Histogram::SampleSet ss = h->SnapshotSample();
+
+ // Get the number of samples in each bucket.
+ for (size_t i = 0; i < bucketCount; i++) {
+ // XXX(Bug 1631371) Check if this should use a fallible operation as it
+ // pretended earlier, or change the return type to void.
+ aSnapshot.mBucketCounts.AppendElement(ss.counts(i));
+ }
+
+ // Finally, save the |sum|. We don't need to reflect declared_min,
+ // declared_max and histogram_type as they are in gHistogramInfo.
+ aSnapshot.mSampleSum = ss.sum();
+ return NS_OK;
+}
+
+/**
+ * Reflect a histogram snapshot into a JavaScript object.
+ * The returned histogram object will have the following properties:
+ *
+ * bucket_count - Number of buckets of this histogram
+ * histogram_type - HISTOGRAM_EXPONENTIAL, HISTOGRAM_LINEAR,
+ * HISTOGRAM_BOOLEAN, HISTOGRAM_FLAG, HISTOGRAM_COUNT, or HISTOGRAM_CATEGORICAL
+ * sum - sum of the bucket contents
+ * range - A 2-item array of minimum and maximum bucket size
+ * values - Map from bucket start to the bucket's count
+ */
+nsresult internal_ReflectHistogramAndSamples(
+ JSContext* cx, JS::Handle<JSObject*> obj,
+ const HistogramInfo& aHistogramInfo,
+ const HistogramSnapshotData& aSnapshot) {
+ if (!(JS_DefineProperty(cx, obj, "bucket_count", aHistogramInfo.bucketCount,
+ JSPROP_ENUMERATE) &&
+ JS_DefineProperty(cx, obj, "histogram_type",
+ aHistogramInfo.histogramType, JSPROP_ENUMERATE) &&
+ JS_DefineProperty(cx, obj, "sum", double(aSnapshot.mSampleSum),
+ JSPROP_ENUMERATE))) {
+ return NS_ERROR_FAILURE;
+ }
+
+ // Don't rely on the bucket counts from "aHistogramInfo": it may
+ // differ from the length of aSnapshot.mBucketCounts due to expired
+ // histograms.
+ const size_t count = aSnapshot.mBucketCounts.Length();
+ MOZ_ASSERT(count == aSnapshot.mBucketRanges.Length(),
+ "The number of buckets and the number of counts must match.");
+
+ // Create the "range" property and add it to the final object.
+ JS::Rooted<JSObject*> rarray(cx, JS::NewArrayObject(cx, 2));
+ if (rarray == nullptr ||
+ !JS_DefineProperty(cx, obj, "range", rarray, JSPROP_ENUMERATE)) {
+ return NS_ERROR_FAILURE;
+ }
+ // Add [min, max] into the range array
+ if (!JS_DefineElement(cx, rarray, 0, aHistogramInfo.min, JSPROP_ENUMERATE)) {
+ return NS_ERROR_FAILURE;
+ }
+ if (!JS_DefineElement(cx, rarray, 1, aHistogramInfo.max, JSPROP_ENUMERATE)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ JS::Rooted<JSObject*> values(cx, JS_NewPlainObject(cx));
+ if (values == nullptr ||
+ !JS_DefineProperty(cx, obj, "values", values, JSPROP_ENUMERATE)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ bool first = true;
+ size_t last = 0;
+
+ for (size_t i = 0; i < count; i++) {
+ auto value = aSnapshot.mBucketCounts[i];
+ if (value == 0) {
+ continue;
+ }
+
+ if (i > 0 && first) {
+ auto range = aSnapshot.mBucketRanges[i - 1];
+ if (!JS_DefineProperty(cx, values, nsPrintfCString("%d", range).get(), 0,
+ JSPROP_ENUMERATE)) {
+ return NS_ERROR_FAILURE;
+ }
+ }
+
+ first = false;
+ last = i + 1;
+
+ auto range = aSnapshot.mBucketRanges[i];
+ if (!JS_DefineProperty(cx, values, nsPrintfCString("%d", range).get(),
+ value, JSPROP_ENUMERATE)) {
+ return NS_ERROR_FAILURE;
+ }
+ }
+
+ if (last > 0 && last < count) {
+ auto range = aSnapshot.mBucketRanges[last];
+ if (!JS_DefineProperty(cx, values, nsPrintfCString("%d", range).get(), 0,
+ JSPROP_ENUMERATE)) {
+ return NS_ERROR_FAILURE;
+ }
+ }
+
+ return NS_OK;
+}
+
+bool internal_ShouldReflectHistogram(const StaticMutexAutoLock& aLock,
+ base::Histogram* h, HistogramID id) {
+ // Only flag histograms are serialized when they are empty.
+ // This has historical reasons, changing this will require downstream changes.
+ // The cheaper path here is to just deprecate flag histograms in favor
+ // of scalars.
+ uint32_t type = gHistogramInfos[id].histogramType;
+ if (internal_IsEmpty(aLock, h) && type != nsITelemetry::HISTOGRAM_FLAG) {
+ return false;
+ }
+
+ // Don't reflect the histogram if it's not allowed in this product.
+ if (!CanRecordProduct(gHistogramInfos[id].products)) {
+ return false;
+ }
+
+ return true;
+}
+
+/**
+ * Helper function to get a snapshot of the histograms.
+ *
+ * @param {aLock} the lock proof.
+ * @param {aStore} the name of the store to snapshot.
+ * @param {aDataset} the dataset for which the snapshot is being requested.
+ * @param {aClearSubsession} whether or not to clear the data after
+ * taking the snapshot.
+ * @param {aIncludeGPU} whether or not to include data for the GPU.
+ * @param {aOutSnapshot} the container in which the snapshot data will be
+ * stored.
+ * @return {nsresult} NS_OK if the snapshot was successfully taken or
+ * NS_ERROR_OUT_OF_MEMORY if it failed to allocate memory.
+ */
+nsresult internal_GetHistogramsSnapshot(
+ const StaticMutexAutoLock& aLock, const nsACString& aStore,
+ unsigned int aDataset, bool aClearSubsession, bool aIncludeGPU,
+ bool aFilterTest, HistogramProcessSnapshotsArray& aOutSnapshot) {
+ if (!aOutSnapshot.resize(static_cast<uint32_t>(ProcessID::Count))) {
+ return NS_ERROR_OUT_OF_MEMORY;
+ }
+
+ for (uint32_t process = 0; process < static_cast<uint32_t>(ProcessID::Count);
+ ++process) {
+ HistogramSnapshotsArray& hArray = aOutSnapshot[process];
+
+ for (size_t i = 0; i < HistogramCount; ++i) {
+ const HistogramInfo& info = gHistogramInfos[i];
+ if (info.keyed) {
+ continue;
+ }
+
+ HistogramID id = HistogramID(i);
+
+ if (!CanRecordInProcess(info.record_in_processes, ProcessID(process)) ||
+ ((ProcessID(process) == ProcessID::Gpu) && !aIncludeGPU)) {
+ continue;
+ }
+
+ if (!IsInDataset(info.dataset, aDataset)) {
+ continue;
+ }
+
+ bool shouldInstantiate =
+ info.histogramType == nsITelemetry::HISTOGRAM_FLAG;
+ Histogram* w = internal_GetHistogramById(aLock, id, ProcessID(process),
+ shouldInstantiate);
+ if (!w || w->IsExpired()) {
+ continue;
+ }
+
+ base::Histogram* h = nullptr;
+ if (!w->GetHistogram(aStore, &h)) {
+ continue;
+ }
+
+ if (!internal_ShouldReflectHistogram(aLock, h, id)) {
+ continue;
+ }
+
+ const char* name = info.name();
+ if (aFilterTest && strncmp(TEST_HISTOGRAM_PREFIX, name,
+ strlen(TEST_HISTOGRAM_PREFIX)) == 0) {
+ if (aClearSubsession) {
+ h->Clear();
+ }
+ continue;
+ }
+
+ HistogramSnapshotData snapshotData;
+ if (NS_FAILED(internal_GetHistogramAndSamples(aLock, h, snapshotData))) {
+ continue;
+ }
+
+ if (!hArray.emplaceBack(HistogramSnapshotInfo{snapshotData, id})) {
+ return NS_ERROR_OUT_OF_MEMORY;
+ }
+
+ if (aClearSubsession) {
+ h->Clear();
+ }
+ }
+ }
+ return NS_OK;
+}
+
+} // namespace
+
+////////////////////////////////////////////////////////////////////////
+////////////////////////////////////////////////////////////////////////
+//
+// PRIVATE: class Histogram
+
+namespace {
+
+Histogram::Histogram(HistogramID histogramId, const HistogramInfo& info,
+ bool expired)
+ : mSingleStore(nullptr), mIsExpired(expired) {
+ if (IsExpired()) {
+ return;
+ }
+
+ const int bucketsOffset = gHistogramBucketLowerBoundIndex[histogramId];
+
+ if (info.is_single_store()) {
+ mSingleStore = internal_CreateBaseHistogramInstance(info, bucketsOffset);
+ } else {
+ for (uint32_t i = 0; i < info.store_count; i++) {
+ auto store = nsDependentCString(
+ &gHistogramStringTable[gHistogramStoresTable[info.store_index + i]]);
+ mStorage.InsertOrUpdate(store, UniquePtr<base::Histogram>(
+ internal_CreateBaseHistogramInstance(
+ info, bucketsOffset)));
+ }
+ }
+}
+
+Histogram::~Histogram() { delete mSingleStore; }
+
+void Histogram::Add(uint32_t sample) {
+ MOZ_ASSERT(XRE_IsParentProcess(),
+ "Only add to histograms in the parent process");
+ if (!XRE_IsParentProcess()) {
+ return;
+ }
+
+ if (IsExpired()) {
+ return;
+ }
+
+ if (mSingleStore != nullptr) {
+ mSingleStore->Add(sample);
+ } else {
+ for (auto iter = mStorage.Iter(); !iter.Done(); iter.Next()) {
+ auto& h = iter.Data();
+ h->Add(sample);
+ }
+ }
+}
+
+void Histogram::Clear(const nsACString& store) {
+ MOZ_ASSERT(XRE_IsParentProcess(),
+ "Only clear histograms in the parent process");
+ if (!XRE_IsParentProcess()) {
+ return;
+ }
+
+ if (mSingleStore != nullptr) {
+ if (store.EqualsASCII("main")) {
+ mSingleStore->Clear();
+ }
+ } else {
+ base::Histogram* h = nullptr;
+ bool found = GetHistogram(store, &h);
+ if (!found) {
+ return;
+ }
+ MOZ_ASSERT(h, "Should have found a valid histogram in the named store");
+
+ h->Clear();
+ }
+}
+
+bool Histogram::GetHistogram(const nsACString& store, base::Histogram** h) {
+ MOZ_ASSERT(!IsExpired());
+ if (IsExpired()) {
+ return false;
+ }
+
+ if (mSingleStore != nullptr) {
+ if (store.EqualsASCII("main")) {
+ *h = mSingleStore;
+ return true;
+ }
+
+ return false;
+ }
+
+ return mStorage.Get(store, h);
+}
+
+size_t Histogram::SizeOfIncludingThis(mozilla::MallocSizeOf aMallocSizeOf) {
+ size_t n = 0;
+ n += aMallocSizeOf(this);
+ /*
+ * In theory mStorage.SizeOfExcludingThis should included the data part of the
+ * map, but the numbers seemed low, so we are only taking the shallow size and
+ * do the iteration here.
+ */
+ n += mStorage.ShallowSizeOfExcludingThis(aMallocSizeOf);
+ for (const auto& h : mStorage.Values()) {
+ n += h->SizeOfIncludingThis(aMallocSizeOf);
+ }
+ if (mSingleStore != nullptr) {
+ // base::Histogram doesn't have SizeOfExcludingThis, so we are overcounting
+ // the pointer here.
+ n += mSingleStore->SizeOfIncludingThis(aMallocSizeOf);
+ }
+ return n;
+}
+
+} // namespace
+
+////////////////////////////////////////////////////////////////////////
+////////////////////////////////////////////////////////////////////////
+//
+// PRIVATE: class KeyedHistogram and internal_ReflectKeyedHistogram
+
+namespace {
+
+nsresult internal_ReflectKeyedHistogram(
+ const KeyedHistogramSnapshotData& aSnapshot, const HistogramInfo& info,
+ JSContext* aCx, JS::Handle<JSObject*> aObj) {
+ for (const auto& entry : aSnapshot) {
+ const HistogramSnapshotData& keyData = entry.GetData();
+
+ JS::Rooted<JSObject*> histogramSnapshot(aCx, JS_NewPlainObject(aCx));
+ if (!histogramSnapshot) {
+ return NS_ERROR_FAILURE;
+ }
+
+ if (NS_FAILED(internal_ReflectHistogramAndSamples(aCx, histogramSnapshot,
+ info, keyData))) {
+ return NS_ERROR_FAILURE;
+ }
+
+ const NS_ConvertUTF8toUTF16 key(entry.GetKey());
+ if (!JS_DefineUCProperty(aCx, aObj, key.Data(), key.Length(),
+ histogramSnapshot, JSPROP_ENUMERATE)) {
+ return NS_ERROR_FAILURE;
+ }
+ }
+
+ return NS_OK;
+}
+
+KeyedHistogram::KeyedHistogram(HistogramID id, const HistogramInfo& info,
+ bool expired)
+ : mSingleStore(nullptr),
+ mId(id),
+ mHistogramInfo(info),
+ mIsExpired(expired) {
+ if (IsExpired()) {
+ return;
+ }
+
+ if (info.is_single_store()) {
+ mSingleStore = new KeyedHistogramMapType;
+ } else {
+ for (uint32_t i = 0; i < info.store_count; i++) {
+ auto store = nsDependentCString(
+ &gHistogramStringTable[gHistogramStoresTable[info.store_index + i]]);
+ mStorage.InsertOrUpdate(store, MakeUnique<KeyedHistogramMapType>());
+ }
+ }
+}
+
+KeyedHistogram::~KeyedHistogram() { delete mSingleStore; }
+
+nsresult KeyedHistogram::GetHistogram(const nsCString& aStore,
+ const nsCString& key,
+ base::Histogram** histogram) {
+ if (IsExpired()) {
+ MOZ_ASSERT(false,
+ "KeyedHistogram::GetHistogram called on an expired histogram.");
+ return NS_ERROR_FAILURE;
+ }
+
+ KeyedHistogramMapType* histogramMap;
+ bool found;
+
+ if (mSingleStore != nullptr) {
+ histogramMap = mSingleStore;
+ } else {
+ found = mStorage.Get(aStore, &histogramMap);
+ if (!found) {
+ return NS_ERROR_FAILURE;
+ }
+ }
+
+ found = histogramMap->Get(key, histogram);
+ if (found) {
+ return NS_OK;
+ }
+
+ int bucketsOffset = gHistogramBucketLowerBoundIndex[mId];
+ auto h = UniquePtr<base::Histogram>{
+ internal_CreateBaseHistogramInstance(mHistogramInfo, bucketsOffset)};
+ if (!h) {
+ return NS_ERROR_FAILURE;
+ }
+
+ h->ClearFlags(base::Histogram::kUmaTargetedHistogramFlag);
+ *histogram = h.get();
+
+ bool inserted =
+ histogramMap->InsertOrUpdate(key, std::move(h), mozilla::fallible);
+ if (MOZ_UNLIKELY(!inserted)) {
+ return NS_ERROR_OUT_OF_MEMORY;
+ }
+ return NS_OK;
+}
+
+base::Histogram* KeyedHistogram::GetHistogram(const nsCString& aStore,
+ const nsCString& key) {
+ base::Histogram* h = nullptr;
+ if (NS_FAILED(GetHistogram(aStore, key, &h))) {
+ return nullptr;
+ }
+ return h;
+}
+
+nsresult KeyedHistogram::Add(const nsCString& key, uint32_t sample,
+ ProcessID aProcessType) {
+ MOZ_ASSERT(XRE_IsParentProcess(),
+ "Only add to keyed histograms in the parent process");
+ if (!XRE_IsParentProcess()) {
+ return NS_ERROR_FAILURE;
+ }
+
+ bool canRecordDataset =
+ CanRecordDataset(mHistogramInfo.dataset, internal_CanRecordBase(),
+ internal_CanRecordExtended());
+ // If `histogram` is a non-parent-process histogram, then recording-enabled
+ // has been checked in its owner process.
+ if (!canRecordDataset || (aProcessType == ProcessID::Parent &&
+ !internal_IsRecordingEnabled(mId))) {
+ return NS_OK;
+ }
+
+ // Don't record if expired.
+ if (IsExpired()) {
+ return NS_OK;
+ }
+
+ // Don't record if the current platform is not enabled
+ if (!CanRecordProduct(gHistogramInfos[mId].products)) {
+ return NS_OK;
+ }
+
+ // The internal representation of a base::Histogram's buckets uses `int`.
+ // Clamp large values of `sample` to be INT_MAX so they continue to be treated
+ // as large values (instead of negative ones).
+ if (sample > INT_MAX) {
+ TelemetryScalar::Add(
+ mozilla::Telemetry::ScalarID::TELEMETRY_ACCUMULATE_CLAMPED_VALUES,
+ NS_ConvertASCIItoUTF16(mHistogramInfo.name()), 1);
+ sample = INT_MAX;
+ }
+
+ base::Histogram* histogram;
+ if (mSingleStore != nullptr) {
+ histogram = GetHistogram("main"_ns, key);
+ if (!histogram) {
+ MOZ_ASSERT(false, "Missing histogram in single store.");
+ return NS_ERROR_FAILURE;
+ }
+
+ histogram->Add(sample);
+ } else {
+ for (uint32_t i = 0; i < mHistogramInfo.store_count; i++) {
+ auto store = nsDependentCString(
+ &gHistogramStringTable
+ [gHistogramStoresTable[mHistogramInfo.store_index + i]]);
+ base::Histogram* histogram = GetHistogram(store, key);
+ MOZ_ASSERT(histogram);
+ if (histogram) {
+ histogram->Add(sample);
+ } else {
+ return NS_ERROR_FAILURE;
+ }
+ }
+ }
+
+ return NS_OK;
+}
+
+void KeyedHistogram::Clear(const nsACString& aStore) {
+ MOZ_ASSERT(XRE_IsParentProcess(),
+ "Only clear keyed histograms in the parent process");
+ if (!XRE_IsParentProcess()) {
+ return;
+ }
+
+ if (IsExpired()) {
+ return;
+ }
+
+ if (mSingleStore) {
+ if (aStore.EqualsASCII("main")) {
+ mSingleStore->Clear();
+ }
+ return;
+ }
+
+ KeyedHistogramMapType* histogramMap;
+ bool found = mStorage.Get(aStore, &histogramMap);
+ if (!found) {
+ return;
+ }
+
+ histogramMap->Clear();
+}
+
+bool KeyedHistogram::IsEmpty(const nsACString& aStore) const {
+ if (mSingleStore != nullptr) {
+ if (aStore.EqualsASCII("main")) {
+ return mSingleStore->IsEmpty();
+ }
+
+ return true;
+ }
+
+ KeyedHistogramMapType* histogramMap;
+ bool found = mStorage.Get(aStore, &histogramMap);
+ if (!found) {
+ return true;
+ }
+ return histogramMap->IsEmpty();
+}
+
+size_t KeyedHistogram::SizeOfIncludingThis(
+ mozilla::MallocSizeOf aMallocSizeOf) {
+ size_t n = 0;
+ n += aMallocSizeOf(this);
+ /*
+ * In theory mStorage.SizeOfExcludingThis should included the data part of the
+ * map, but the numbers seemed low, so we are only taking the shallow size and
+ * do the iteration here.
+ */
+ n += mStorage.ShallowSizeOfExcludingThis(aMallocSizeOf);
+ for (const auto& h : mStorage.Values()) {
+ n += h->SizeOfIncludingThis(aMallocSizeOf);
+ }
+ if (mSingleStore != nullptr) {
+ // base::Histogram doesn't have SizeOfExcludingThis, so we are overcounting
+ // the pointer here.
+ n += mSingleStore->SizeOfExcludingThis(aMallocSizeOf);
+ }
+ return n;
+}
+
+nsresult KeyedHistogram::GetKeys(const StaticMutexAutoLock& aLock,
+ const nsCString& store,
+ nsTArray<nsCString>& aKeys) {
+ KeyedHistogramMapType* histogramMap;
+ if (mSingleStore != nullptr) {
+ histogramMap = mSingleStore;
+ } else {
+ bool found = mStorage.Get(store, &histogramMap);
+ if (!found) {
+ return NS_ERROR_FAILURE;
+ }
+ }
+
+ if (!aKeys.SetCapacity(histogramMap->Count(), mozilla::fallible)) {
+ return NS_ERROR_OUT_OF_MEMORY;
+ }
+
+ for (const auto& key : histogramMap->Keys()) {
+ if (!aKeys.AppendElement(key, mozilla::fallible)) {
+ return NS_ERROR_OUT_OF_MEMORY;
+ }
+ }
+
+ return NS_OK;
+}
+
+nsresult KeyedHistogram::GetJSSnapshot(JSContext* cx, JS::Handle<JSObject*> obj,
+ const nsACString& aStore,
+ bool clearSubsession) {
+ // Get a snapshot of the data.
+ KeyedHistogramSnapshotData dataSnapshot;
+ {
+ StaticMutexAutoLock locker(gTelemetryHistogramMutex);
+ MOZ_ASSERT(internal_IsHistogramEnumId(mId));
+
+ // Take a snapshot of the data here, protected by the lock, and then,
+ // outside of the lock protection, mirror it to a JS structure.
+ nsresult rv = GetSnapshot(locker, aStore, dataSnapshot, clearSubsession);
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+ }
+
+ // Now that we have a copy of the data, mirror it to JS.
+ return internal_ReflectKeyedHistogram(dataSnapshot, gHistogramInfos[mId], cx,
+ obj);
+}
+
+/**
+ * Return a histogram snapshot for the named store.
+ *
+ * If getting the snapshot succeeds, NS_OK is returned and `aSnapshot` contains
+ * the snapshot data. If the histogram is not available in the named store,
+ * NS_ERROR_NO_CONTENT is returned. For other errors, NS_ERROR_FAILURE is
+ * returned.
+ */
+nsresult KeyedHistogram::GetSnapshot(const StaticMutexAutoLock& aLock,
+ const nsACString& aStore,
+ KeyedHistogramSnapshotData& aSnapshot,
+ bool aClearSubsession) {
+ KeyedHistogramMapType* histogramMap;
+ if (mSingleStore != nullptr) {
+ if (!aStore.EqualsASCII("main")) {
+ return NS_ERROR_NO_CONTENT;
+ }
+
+ histogramMap = mSingleStore;
+ } else {
+ bool found = mStorage.Get(aStore, &histogramMap);
+ if (!found) {
+ // Nothing in the main store is fine, it's just handled as empty
+ return NS_ERROR_NO_CONTENT;
+ }
+ }
+
+ // Snapshot every key.
+ for (const auto& entry : *histogramMap) {
+ base::Histogram* keyData = entry.GetWeak();
+ if (!keyData) {
+ return NS_ERROR_FAILURE;
+ }
+
+ HistogramSnapshotData keySnapshot;
+ if (NS_FAILED(
+ internal_GetHistogramAndSamples(aLock, keyData, keySnapshot))) {
+ return NS_ERROR_FAILURE;
+ }
+
+ // Append to the final snapshot.
+ aSnapshot.InsertOrUpdate(entry.GetKey(), std::move(keySnapshot));
+ }
+
+ if (aClearSubsession) {
+ Clear(aStore);
+ }
+
+ return NS_OK;
+}
+
+/**
+ * Helper function to get a snapshot of the keyed histograms.
+ *
+ * @param {aLock} the lock proof.
+ * @param {aDataset} the dataset for which the snapshot is being requested.
+ * @param {aClearSubsession} whether or not to clear the data after
+ * taking the snapshot.
+ * @param {aIncludeGPU} whether or not to include data for the GPU.
+ * @param {aOutSnapshot} the container in which the snapshot data will be
+ * stored.
+ * @return {nsresult} NS_OK if the snapshot was successfully taken or
+ * NS_ERROR_OUT_OF_MEMORY if it failed to allocate memory.
+ */
+nsresult internal_GetKeyedHistogramsSnapshot(
+ const StaticMutexAutoLock& aLock, const nsACString& aStore,
+ unsigned int aDataset, bool aClearSubsession, bool aIncludeGPU,
+ bool aFilterTest, KeyedHistogramProcessSnapshotsArray& aOutSnapshot) {
+ if (!aOutSnapshot.resize(static_cast<uint32_t>(ProcessID::Count))) {
+ return NS_ERROR_OUT_OF_MEMORY;
+ }
+
+ for (uint32_t process = 0; process < static_cast<uint32_t>(ProcessID::Count);
+ ++process) {
+ KeyedHistogramSnapshotsArray& hArray = aOutSnapshot[process];
+
+ for (size_t i = 0; i < HistogramCount; ++i) {
+ HistogramID id = HistogramID(i);
+ const HistogramInfo& info = gHistogramInfos[id];
+ if (!info.keyed) {
+ continue;
+ }
+
+ if (!CanRecordInProcess(info.record_in_processes, ProcessID(process)) ||
+ ((ProcessID(process) == ProcessID::Gpu) && !aIncludeGPU)) {
+ continue;
+ }
+
+ if (!IsInDataset(info.dataset, aDataset)) {
+ continue;
+ }
+
+ KeyedHistogram* keyed =
+ internal_GetKeyedHistogramById(id, ProcessID(process),
+ /* instantiate = */ false);
+ if (!keyed || keyed->IsEmpty(aStore) || keyed->IsExpired()) {
+ continue;
+ }
+
+ const char* name = info.name();
+ if (aFilterTest && strncmp(TEST_HISTOGRAM_PREFIX, name,
+ strlen(TEST_HISTOGRAM_PREFIX)) == 0) {
+ if (aClearSubsession) {
+ keyed->Clear(aStore);
+ }
+ continue;
+ }
+
+ // Take a snapshot of the keyed histogram data!
+ KeyedHistogramSnapshotData snapshot;
+ if (!NS_SUCCEEDED(
+ keyed->GetSnapshot(aLock, aStore, snapshot, aClearSubsession))) {
+ return NS_ERROR_FAILURE;
+ }
+
+ if (!hArray.emplaceBack(
+ KeyedHistogramSnapshotInfo{std::move(snapshot), id})) {
+ return NS_ERROR_OUT_OF_MEMORY;
+ }
+ }
+ }
+ return NS_OK;
+}
+
+} // namespace
+
+////////////////////////////////////////////////////////////////////////
+////////////////////////////////////////////////////////////////////////
+//
+// PRIVATE: thread-unsafe helpers for the external interface
+
+namespace {
+
+bool internal_RemoteAccumulate(const StaticMutexAutoLock& aLock,
+ HistogramID aId, uint32_t aSample) {
+ if (XRE_IsParentProcess()) {
+ return false;
+ }
+
+ if (!internal_IsRecordingEnabled(aId)) {
+ return true;
+ }
+
+ TelemetryIPCAccumulator::AccumulateChildHistogram(aId, aSample);
+ return true;
+}
+
+bool internal_RemoteAccumulate(const StaticMutexAutoLock& aLock,
+ HistogramID aId, const nsCString& aKey,
+ uint32_t aSample) {
+ if (XRE_IsParentProcess()) {
+ return false;
+ }
+
+ if (!internal_IsRecordingEnabled(aId)) {
+ return true;
+ }
+
+ TelemetryIPCAccumulator::AccumulateChildKeyedHistogram(aId, aKey, aSample);
+ return true;
+}
+
+void internal_Accumulate(const StaticMutexAutoLock& aLock, HistogramID aId,
+ uint32_t aSample) {
+ if (!internal_CanRecordBase() ||
+ internal_RemoteAccumulate(aLock, aId, aSample)) {
+ return;
+ }
+
+ Histogram* w = internal_GetHistogramById(aLock, aId, ProcessID::Parent);
+ MOZ_ASSERT(w);
+ internal_HistogramAdd(aLock, *w, aId, aSample, ProcessID::Parent);
+}
+
+void internal_Accumulate(const StaticMutexAutoLock& aLock, HistogramID aId,
+ const nsCString& aKey, uint32_t aSample) {
+ if (!gInitDone || !internal_CanRecordBase() ||
+ internal_RemoteAccumulate(aLock, aId, aKey, aSample)) {
+ return;
+ }
+
+ KeyedHistogram* keyed =
+ internal_GetKeyedHistogramById(aId, ProcessID::Parent);
+ MOZ_ASSERT(keyed);
+ keyed->Add(aKey, aSample, ProcessID::Parent);
+}
+
+void internal_AccumulateChild(const StaticMutexAutoLock& aLock,
+ ProcessID aProcessType, HistogramID aId,
+ uint32_t aSample) {
+ if (!internal_CanRecordBase()) {
+ return;
+ }
+
+ Histogram* w = internal_GetHistogramById(aLock, aId, aProcessType);
+ if (w == nullptr) {
+ NS_WARNING("Failed GetHistogramById for CHILD");
+ } else {
+ internal_HistogramAdd(aLock, *w, aId, aSample, aProcessType);
+ }
+}
+
+void internal_AccumulateChildKeyed(const StaticMutexAutoLock& aLock,
+ ProcessID aProcessType, HistogramID aId,
+ const nsCString& aKey, uint32_t aSample) {
+ if (!gInitDone || !internal_CanRecordBase()) {
+ return;
+ }
+
+ KeyedHistogram* keyed = internal_GetKeyedHistogramById(aId, aProcessType);
+ MOZ_ASSERT(keyed);
+ keyed->Add(aKey, aSample, aProcessType);
+}
+
+void internal_ClearHistogram(const StaticMutexAutoLock& aLock, HistogramID id,
+ const nsACString& aStore) {
+ MOZ_ASSERT(XRE_IsParentProcess());
+ if (!XRE_IsParentProcess()) {
+ return;
+ }
+
+ // Handle keyed histograms.
+ if (gHistogramInfos[id].keyed) {
+ for (uint32_t process = 0;
+ process < static_cast<uint32_t>(ProcessID::Count); ++process) {
+ KeyedHistogram* kh = internal_GetKeyedHistogramById(
+ id, static_cast<ProcessID>(process), /* instantiate = */ false);
+ if (kh) {
+ kh->Clear(aStore);
+ }
+ }
+ } else {
+ // Reset the histograms instances for all processes.
+ for (uint32_t process = 0;
+ process < static_cast<uint32_t>(ProcessID::Count); ++process) {
+ Histogram* h =
+ internal_GetHistogramById(aLock, id, static_cast<ProcessID>(process),
+ /* instantiate = */ false);
+ if (h) {
+ h->Clear(aStore);
+ }
+ }
+ }
+}
+
+} // namespace
+
+////////////////////////////////////////////////////////////////////////
+////////////////////////////////////////////////////////////////////////
+//
+// PRIVATE: JSHistogram_* functions
+
+// NOTE: the functions in this section:
+//
+// internal_JSHistogram_Add
+// internal_JSHistogram_Name
+// internal_JSHistogram_Snapshot
+// internal_JSHistogram_Clear
+// internal_WrapAndReturnHistogram
+//
+// all run without protection from |gTelemetryHistogramMutex|. If they
+// held |gTelemetryHistogramMutex|, there would be the possibility of
+// deadlock because the JS_ calls that they make may call back into the
+// TelemetryHistogram interface, hence trying to re-acquire the mutex.
+//
+// This means that these functions potentially race against threads, but
+// that seems preferable to risking deadlock.
+
+namespace {
+
+static constexpr uint32_t HistogramObjectDataSlot = 0;
+static constexpr uint32_t HistogramObjectSlotCount =
+ HistogramObjectDataSlot + 1;
+
+void internal_JSHistogram_finalize(JS::GCContext*, JSObject*);
+
+static const JSClassOps sJSHistogramClassOps = {nullptr, /* addProperty */
+ nullptr, /* delProperty */
+ nullptr, /* enumerate */
+ nullptr, /* newEnumerate */
+ nullptr, /* resolve */
+ nullptr, /* mayResolve */
+ internal_JSHistogram_finalize};
+
+static const JSClass sJSHistogramClass = {
+ "JSHistogram", /* name */
+ JSCLASS_HAS_RESERVED_SLOTS(HistogramObjectSlotCount) |
+ JSCLASS_FOREGROUND_FINALIZE, /* flags */
+ &sJSHistogramClassOps};
+
+struct JSHistogramData {
+ HistogramID histogramId;
+};
+
+bool internal_JSHistogram_CoerceValue(JSContext* aCx,
+ JS::Handle<JS::Value> aElement,
+ HistogramID aId, uint32_t aHistogramType,
+ uint32_t& aValue) {
+ if (aElement.isString()) {
+ // Strings only allowed for categorical histograms
+ if (aHistogramType != nsITelemetry::HISTOGRAM_CATEGORICAL) {
+ LogToBrowserConsole(
+ nsIScriptError::errorFlag,
+ nsLiteralString(
+ u"String argument only allowed for categorical histogram"));
+ return false;
+ }
+
+ // Label is given by the string argument
+ nsAutoJSString label;
+ if (!label.init(aCx, aElement)) {
+ LogToBrowserConsole(nsIScriptError::errorFlag,
+ u"Invalid string parameter"_ns);
+ return false;
+ }
+
+ // Get the label id for accumulation
+ nsresult rv = gHistogramInfos[aId].label_id(
+ NS_ConvertUTF16toUTF8(label).get(), &aValue);
+ if (NS_FAILED(rv)) {
+ nsPrintfCString msg("'%s' is an invalid string label",
+ NS_ConvertUTF16toUTF8(label).get());
+ LogToBrowserConsole(nsIScriptError::errorFlag,
+ NS_ConvertUTF8toUTF16(msg));
+ return false;
+ }
+ } else if (!(aElement.isNumber() || aElement.isBoolean())) {
+ LogToBrowserConsole(nsIScriptError::errorFlag, u"Argument not a number"_ns);
+ return false;
+ } else if (aElement.isNumber() && aElement.toNumber() > UINT32_MAX) {
+ // Clamp large numerical arguments to aValue's acceptable values.
+ // JS::ToUint32 will take aElement modulo 2^32 before returning it, which
+ // may result in a smaller final value.
+ aValue = UINT32_MAX;
+#ifdef DEBUG
+ LogToBrowserConsole(nsIScriptError::errorFlag,
+ u"Clamped large numeric value"_ns);
+#endif
+ } else if (!JS::ToUint32(aCx, aElement, &aValue)) {
+ LogToBrowserConsole(nsIScriptError::errorFlag,
+ u"Failed to convert element to UInt32"_ns);
+ return false;
+ }
+
+ // If we're here then all type checks have passed and aValue contains the
+ // coerced integer
+ return true;
+}
+
+bool internal_JSHistogram_GetValueArray(JSContext* aCx, JS::CallArgs& args,
+ uint32_t aHistogramType,
+ HistogramID aId, bool isKeyed,
+ nsTArray<uint32_t>& aArray) {
+ // This function populates aArray with the values extracted from args. Handles
+ // keyed and non-keyed histograms, and single and array of values. Also
+ // performs sanity checks on the arguments. Returns true upon successful
+ // population, false otherwise.
+
+ uint32_t firstArgIndex = 0;
+ if (isKeyed) {
+ firstArgIndex = 1;
+ }
+
+ // Special case of no argument (or only key) and count histogram
+ if (args.length() == firstArgIndex) {
+ if (!(aHistogramType == nsITelemetry::HISTOGRAM_COUNT)) {
+ LogToBrowserConsole(
+ nsIScriptError::errorFlag,
+ nsLiteralString(
+ u"Need at least one argument for non count type histogram"));
+ return false;
+ }
+
+ aArray.AppendElement(1);
+ return true;
+ }
+
+ if (args[firstArgIndex].isObject() && !args[firstArgIndex].isString()) {
+ JS::Rooted<JSObject*> arrayObj(aCx, &args[firstArgIndex].toObject());
+
+ bool isArray = false;
+ JS::IsArrayObject(aCx, arrayObj, &isArray);
+
+ if (!isArray) {
+ LogToBrowserConsole(
+ nsIScriptError::errorFlag,
+ nsLiteralString(
+ u"The argument to accumulate can't be a non-array object"));
+ return false;
+ }
+
+ uint32_t arrayLength = 0;
+ if (!JS::GetArrayLength(aCx, arrayObj, &arrayLength)) {
+ LogToBrowserConsole(nsIScriptError::errorFlag,
+ u"Failed while trying to get array length"_ns);
+ return false;
+ }
+
+ for (uint32_t arrayIdx = 0; arrayIdx < arrayLength; arrayIdx++) {
+ JS::Rooted<JS::Value> element(aCx);
+
+ if (!JS_GetElement(aCx, arrayObj, arrayIdx, &element)) {
+ nsPrintfCString msg("Failed while trying to get element at index %d",
+ arrayIdx);
+ LogToBrowserConsole(nsIScriptError::errorFlag,
+ NS_ConvertUTF8toUTF16(msg));
+ return false;
+ }
+
+ uint32_t value = 0;
+ if (!internal_JSHistogram_CoerceValue(aCx, element, aId, aHistogramType,
+ value)) {
+ nsPrintfCString msg("Element at index %d failed type checks", arrayIdx);
+ LogToBrowserConsole(nsIScriptError::errorFlag,
+ NS_ConvertUTF8toUTF16(msg));
+ return false;
+ }
+ aArray.AppendElement(value);
+ }
+
+ return true;
+ }
+
+ uint32_t value = 0;
+ if (!internal_JSHistogram_CoerceValue(aCx, args[firstArgIndex], aId,
+ aHistogramType, value)) {
+ return false;
+ }
+ aArray.AppendElement(value);
+ return true;
+}
+
+static JSHistogramData* GetJSHistogramData(JSObject* obj) {
+ MOZ_ASSERT(JS::GetClass(obj) == &sJSHistogramClass);
+ return JS::GetMaybePtrFromReservedSlot<JSHistogramData>(
+ obj, HistogramObjectDataSlot);
+}
+
+bool internal_JSHistogram_Add(JSContext* cx, unsigned argc, JS::Value* vp) {
+ JS::CallArgs args = CallArgsFromVp(argc, vp);
+
+ if (!args.thisv().isObject() ||
+ JS::GetClass(&args.thisv().toObject()) != &sJSHistogramClass) {
+ JS_ReportErrorASCII(cx, "Wrong JS class, expected JSHistogram class");
+ return false;
+ }
+
+ JSObject* obj = &args.thisv().toObject();
+ JSHistogramData* data = GetJSHistogramData(obj);
+ MOZ_ASSERT(data);
+ HistogramID id = data->histogramId;
+ MOZ_ASSERT(internal_IsHistogramEnumId(id));
+ uint32_t type = gHistogramInfos[id].histogramType;
+
+ // This function should always return |undefined| and never fail but
+ // rather report failures using the console.
+ args.rval().setUndefined();
+
+ nsTArray<uint32_t> values;
+ if (!internal_JSHistogram_GetValueArray(cx, args, type, id, false, values)) {
+ // Either GetValueArray or CoerceValue utility function will have printed a
+ // meaningful error message, so we simply return true
+ return true;
+ }
+
+ {
+ StaticMutexAutoLock locker(gTelemetryHistogramMutex);
+ for (uint32_t aValue : values) {
+ internal_Accumulate(locker, id, aValue);
+ }
+ }
+ return true;
+}
+
+bool internal_JSHistogram_Name(JSContext* cx, unsigned argc, JS::Value* vp) {
+ JS::CallArgs args = CallArgsFromVp(argc, vp);
+
+ if (!args.thisv().isObject() ||
+ JS::GetClass(&args.thisv().toObject()) != &sJSHistogramClass) {
+ JS_ReportErrorASCII(cx, "Wrong JS class, expected JSHistogram class");
+ return false;
+ }
+
+ JSObject* obj = &args.thisv().toObject();
+ JSHistogramData* data = GetJSHistogramData(obj);
+ MOZ_ASSERT(data);
+ HistogramID id = data->histogramId;
+ MOZ_ASSERT(internal_IsHistogramEnumId(id));
+ const char* name = gHistogramInfos[id].name();
+
+ auto cname = NS_ConvertASCIItoUTF16(name);
+ args.rval().setString(ToJSString(cx, cname));
+
+ return true;
+}
+
+/**
+ * Extract the store name from JavaScript function arguments.
+ * The first and only argument needs to be an object with a "store" property.
+ * If no arguments are given it defaults to "main".
+ */
+nsresult internal_JS_StoreFromObjectArgument(JSContext* cx,
+ const JS::CallArgs& args,
+ nsAutoString& aStoreName) {
+ if (args.length() == 0) {
+ aStoreName.AssignLiteral("main");
+ } else if (args.length() == 1) {
+ if (!args[0].isObject()) {
+ JS_ReportErrorASCII(cx, "Expected object argument.");
+ return NS_ERROR_FAILURE;
+ }
+
+ JS::Rooted<JS::Value> storeValue(cx);
+ JS::Rooted<JSObject*> argsObject(cx, &args[0].toObject());
+ if (!JS_GetProperty(cx, argsObject, "store", &storeValue)) {
+ JS_ReportErrorASCII(cx,
+ "Expected object argument to have property 'store'.");
+ return NS_ERROR_FAILURE;
+ }
+
+ nsAutoJSString store;
+ if (!storeValue.isString() || !store.init(cx, storeValue)) {
+ JS_ReportErrorASCII(
+ cx, "Expected object argument's 'store' property to be a string.");
+ return NS_ERROR_FAILURE;
+ }
+
+ aStoreName.Assign(store);
+ } else {
+ JS_ReportErrorASCII(cx, "Expected at most one argument.");
+ return NS_ERROR_FAILURE;
+ }
+
+ return NS_OK;
+}
+
+bool internal_JSHistogram_Snapshot(JSContext* cx, unsigned argc,
+ JS::Value* vp) {
+ JS::CallArgs args = JS::CallArgsFromVp(argc, vp);
+
+ if (!XRE_IsParentProcess()) {
+ JS_ReportErrorASCII(
+ cx, "Histograms can only be snapshotted in the parent process");
+ return false;
+ }
+
+ if (!args.thisv().isObject() ||
+ JS::GetClass(&args.thisv().toObject()) != &sJSHistogramClass) {
+ JS_ReportErrorASCII(cx, "Wrong JS class, expected JSHistogram class");
+ return false;
+ }
+
+ JSObject* obj = &args.thisv().toObject();
+ JSHistogramData* data = GetJSHistogramData(obj);
+ MOZ_ASSERT(data);
+ HistogramID id = data->histogramId;
+
+ nsAutoString storeName;
+ nsresult rv = internal_JS_StoreFromObjectArgument(cx, args, storeName);
+ if (NS_FAILED(rv)) {
+ return false;
+ }
+
+ HistogramSnapshotData dataSnapshot;
+ {
+ StaticMutexAutoLock locker(gTelemetryHistogramMutex);
+ MOZ_ASSERT(internal_IsHistogramEnumId(id));
+
+ // This is not good standard behavior given that we have histogram instances
+ // covering multiple processes.
+ // However, changing this requires some broader changes to callers.
+ Histogram* w = internal_GetHistogramById(locker, id, ProcessID::Parent);
+ base::Histogram* h = nullptr;
+ if (!w->GetHistogram(NS_ConvertUTF16toUTF8(storeName), &h)) {
+ // When it's not in the named store, let's skip the snapshot completely,
+ // but don't fail
+ args.rval().setUndefined();
+ return true;
+ }
+ // Take a snapshot of the data here, protected by the lock, and then,
+ // outside of the lock protection, mirror it to a JS structure
+ if (NS_FAILED(internal_GetHistogramAndSamples(locker, h, dataSnapshot))) {
+ return false;
+ }
+ }
+
+ JS::Rooted<JSObject*> snapshot(cx, JS_NewPlainObject(cx));
+ if (!snapshot) {
+ return false;
+ }
+
+ if (NS_FAILED(internal_ReflectHistogramAndSamples(
+ cx, snapshot, gHistogramInfos[id], dataSnapshot))) {
+ return false;
+ }
+
+ args.rval().setObject(*snapshot);
+ return true;
+}
+
+bool internal_JSHistogram_Clear(JSContext* cx, unsigned argc, JS::Value* vp) {
+ if (!XRE_IsParentProcess()) {
+ JS_ReportErrorASCII(cx,
+ "Histograms can only be cleared in the parent process");
+ return false;
+ }
+
+ JS::CallArgs args = JS::CallArgsFromVp(argc, vp);
+
+ if (!args.thisv().isObject() ||
+ JS::GetClass(&args.thisv().toObject()) != &sJSHistogramClass) {
+ JS_ReportErrorASCII(cx, "Wrong JS class, expected JSHistogram class");
+ return false;
+ }
+
+ JSObject* obj = &args.thisv().toObject();
+ JSHistogramData* data = GetJSHistogramData(obj);
+ MOZ_ASSERT(data);
+
+ nsAutoString storeName;
+ nsresult rv = internal_JS_StoreFromObjectArgument(cx, args, storeName);
+ if (NS_FAILED(rv)) {
+ return false;
+ }
+
+ // This function should always return |undefined| and never fail but
+ // rather report failures using the console.
+ args.rval().setUndefined();
+
+ HistogramID id = data->histogramId;
+ {
+ StaticMutexAutoLock locker(gTelemetryHistogramMutex);
+
+ MOZ_ASSERT(internal_IsHistogramEnumId(id));
+ internal_ClearHistogram(locker, id, NS_ConvertUTF16toUTF8(storeName));
+ }
+
+ return true;
+}
+
+// NOTE: Runs without protection from |gTelemetryHistogramMutex|.
+// See comment at the top of this section.
+nsresult internal_WrapAndReturnHistogram(HistogramID id, JSContext* cx,
+ JS::MutableHandle<JS::Value> ret) {
+ JS::Rooted<JSObject*> obj(cx, JS_NewObject(cx, &sJSHistogramClass));
+ if (!obj) {
+ return NS_ERROR_FAILURE;
+ }
+
+ // The 3 functions that are wrapped up here are eventually called
+ // by the same thread that runs this function.
+ if (!(JS_DefineFunction(cx, obj, "add", internal_JSHistogram_Add, 1, 0) &&
+ JS_DefineFunction(cx, obj, "name", internal_JSHistogram_Name, 1, 0) &&
+ JS_DefineFunction(cx, obj, "snapshot", internal_JSHistogram_Snapshot, 1,
+ 0) &&
+ JS_DefineFunction(cx, obj, "clear", internal_JSHistogram_Clear, 1,
+ 0))) {
+ return NS_ERROR_FAILURE;
+ }
+
+ JSHistogramData* data = new JSHistogramData{id};
+ JS::SetReservedSlot(obj, HistogramObjectDataSlot, JS::PrivateValue(data));
+ ret.setObject(*obj);
+
+ return NS_OK;
+}
+
+void internal_JSHistogram_finalize(JS::GCContext* gcx, JSObject* obj) {
+ if (!obj || JS::GetClass(obj) != &sJSHistogramClass) {
+ MOZ_ASSERT_UNREACHABLE("Should have the right JS class.");
+ return;
+ }
+
+ JSHistogramData* data = GetJSHistogramData(obj);
+ MOZ_ASSERT(data);
+ delete data;
+}
+
+} // namespace
+
+////////////////////////////////////////////////////////////////////////
+////////////////////////////////////////////////////////////////////////
+//
+// PRIVATE: JSKeyedHistogram_* functions
+
+// NOTE: the functions in this section:
+//
+// internal_JSKeyedHistogram_Add
+// internal_JSKeyedHistogram_Name
+// internal_JSKeyedHistogram_Keys
+// internal_JSKeyedHistogram_Snapshot
+// internal_JSKeyedHistogram_Clear
+// internal_WrapAndReturnKeyedHistogram
+//
+// Same comments as above, at the JSHistogram_* section, regarding
+// deadlock avoidance, apply.
+
+namespace {
+
+void internal_JSKeyedHistogram_finalize(JS::GCContext*, JSObject*);
+
+static const JSClassOps sJSKeyedHistogramClassOps = {
+ nullptr, /* addProperty */
+ nullptr, /* delProperty */
+ nullptr, /* enumerate */
+ nullptr, /* newEnumerate */
+ nullptr, /* resolve */
+ nullptr, /* mayResolve */
+ internal_JSKeyedHistogram_finalize};
+
+static const JSClass sJSKeyedHistogramClass = {
+ "JSKeyedHistogram", /* name */
+ JSCLASS_HAS_RESERVED_SLOTS(HistogramObjectSlotCount) |
+ JSCLASS_FOREGROUND_FINALIZE, /* flags */
+ &sJSKeyedHistogramClassOps};
+
+static JSHistogramData* GetJSKeyedHistogramData(JSObject* obj) {
+ MOZ_ASSERT(JS::GetClass(obj) == &sJSKeyedHistogramClass);
+ return JS::GetMaybePtrFromReservedSlot<JSHistogramData>(
+ obj, HistogramObjectDataSlot);
+}
+
+bool internal_JSKeyedHistogram_Snapshot(JSContext* cx, unsigned argc,
+ JS::Value* vp) {
+ if (!XRE_IsParentProcess()) {
+ JS_ReportErrorASCII(
+ cx, "Keyed histograms can only be snapshotted in the parent process");
+ return false;
+ }
+
+ JS::CallArgs args = JS::CallArgsFromVp(argc, vp);
+
+ if (!args.thisv().isObject() ||
+ JS::GetClass(&args.thisv().toObject()) != &sJSKeyedHistogramClass) {
+ JS_ReportErrorASCII(cx, "Wrong JS class, expected JSKeyedHistogram class");
+ return false;
+ }
+
+ JSObject* obj = &args.thisv().toObject();
+ JSHistogramData* data = GetJSKeyedHistogramData(obj);
+ MOZ_ASSERT(data);
+ HistogramID id = data->histogramId;
+ MOZ_ASSERT(internal_IsHistogramEnumId(id));
+
+ // This function should always return |undefined| and never fail but
+ // rather report failures using the console.
+ args.rval().setUndefined();
+
+ // This is not good standard behavior given that we have histogram instances
+ // covering multiple processes.
+ // However, changing this requires some broader changes to callers.
+ KeyedHistogram* keyed = internal_GetKeyedHistogramById(
+ id, ProcessID::Parent, /* instantiate = */ true);
+ if (!keyed) {
+ JS_ReportErrorASCII(cx, "Failed to look up keyed histogram");
+ return false;
+ }
+
+ nsAutoString storeName;
+ nsresult rv;
+ rv = internal_JS_StoreFromObjectArgument(cx, args, storeName);
+ if (NS_FAILED(rv)) {
+ return false;
+ }
+
+ JS::Rooted<JSObject*> snapshot(cx, JS_NewPlainObject(cx));
+ if (!snapshot) {
+ JS_ReportErrorASCII(cx, "Failed to create object");
+ return false;
+ }
+
+ rv = keyed->GetJSSnapshot(cx, snapshot, NS_ConvertUTF16toUTF8(storeName),
+ false);
+
+ // If the store is not available, we return nothing and don't fail
+ if (rv == NS_ERROR_NO_CONTENT) {
+ args.rval().setUndefined();
+ return true;
+ }
+
+ if (!NS_SUCCEEDED(rv)) {
+ JS_ReportErrorASCII(cx, "Failed to reflect keyed histograms");
+ return false;
+ }
+
+ args.rval().setObject(*snapshot);
+ return true;
+}
+
+bool internal_JSKeyedHistogram_Add(JSContext* cx, unsigned argc,
+ JS::Value* vp) {
+ JS::CallArgs args = JS::CallArgsFromVp(argc, vp);
+
+ if (!args.thisv().isObject() ||
+ JS::GetClass(&args.thisv().toObject()) != &sJSKeyedHistogramClass) {
+ JS_ReportErrorASCII(cx, "Wrong JS class, expected JSKeyedHistogram class");
+ return false;
+ }
+
+ JSObject* obj = &args.thisv().toObject();
+ JSHistogramData* data = GetJSKeyedHistogramData(obj);
+ MOZ_ASSERT(data);
+ HistogramID id = data->histogramId;
+ MOZ_ASSERT(internal_IsHistogramEnumId(id));
+
+ // This function should always return |undefined| and never fail but
+ // rather report failures using the console.
+ args.rval().setUndefined();
+ if (args.length() < 1) {
+ LogToBrowserConsole(nsIScriptError::errorFlag, u"Expected one argument"_ns);
+ return true;
+ }
+
+ nsAutoJSString key;
+ if (!args[0].isString() || !key.init(cx, args[0])) {
+ LogToBrowserConsole(nsIScriptError::errorFlag, u"Not a string"_ns);
+ return true;
+ }
+
+ // Check if we're allowed to record in the provided key, for this histogram.
+ if (!gHistogramInfos[id].allows_key(NS_ConvertUTF16toUTF8(key))) {
+ nsPrintfCString msg("%s - key '%s' not allowed for this keyed histogram",
+ gHistogramInfos[id].name(),
+ NS_ConvertUTF16toUTF8(key).get());
+ LogToBrowserConsole(nsIScriptError::errorFlag, NS_ConvertUTF8toUTF16(msg));
+ TelemetryScalar::Add(mozilla::Telemetry::ScalarID::
+ TELEMETRY_ACCUMULATE_UNKNOWN_HISTOGRAM_KEYS,
+ NS_ConvertASCIItoUTF16(gHistogramInfos[id].name()), 1);
+ return true;
+ }
+
+ const uint32_t type = gHistogramInfos[id].histogramType;
+
+ nsTArray<uint32_t> values;
+ if (!internal_JSHistogram_GetValueArray(cx, args, type, id, true, values)) {
+ // Either GetValueArray or CoerceValue utility function will have printed a
+ // meaningful error message so we simple return true
+ return true;
+ }
+
+ {
+ StaticMutexAutoLock locker(gTelemetryHistogramMutex);
+ for (uint32_t aValue : values) {
+ internal_Accumulate(locker, id, NS_ConvertUTF16toUTF8(key), aValue);
+ }
+ }
+ return true;
+}
+
+bool internal_JSKeyedHistogram_Name(JSContext* cx, unsigned argc,
+ JS::Value* vp) {
+ JS::CallArgs args = CallArgsFromVp(argc, vp);
+
+ if (!args.thisv().isObject() ||
+ JS::GetClass(&args.thisv().toObject()) != &sJSKeyedHistogramClass) {
+ JS_ReportErrorASCII(cx, "Wrong JS class, expected JSKeyedHistogram class");
+ return false;
+ }
+
+ JSObject* obj = &args.thisv().toObject();
+ JSHistogramData* data = GetJSKeyedHistogramData(obj);
+ MOZ_ASSERT(data);
+ HistogramID id = data->histogramId;
+ MOZ_ASSERT(internal_IsHistogramEnumId(id));
+ const char* name = gHistogramInfos[id].name();
+
+ auto cname = NS_ConvertASCIItoUTF16(name);
+ args.rval().setString(ToJSString(cx, cname));
+
+ return true;
+}
+
+bool internal_JSKeyedHistogram_Keys(JSContext* cx, unsigned argc,
+ JS::Value* vp) {
+ JS::CallArgs args = JS::CallArgsFromVp(argc, vp);
+
+ if (!args.thisv().isObject() ||
+ JS::GetClass(&args.thisv().toObject()) != &sJSKeyedHistogramClass) {
+ JS_ReportErrorASCII(cx, "Wrong JS class, expected JSKeyedHistogram class");
+ return false;
+ }
+
+ JSObject* obj = &args.thisv().toObject();
+ JSHistogramData* data = GetJSKeyedHistogramData(obj);
+ MOZ_ASSERT(data);
+ HistogramID id = data->histogramId;
+
+ nsAutoString storeName;
+ nsresult rv = internal_JS_StoreFromObjectArgument(cx, args, storeName);
+ if (NS_FAILED(rv)) {
+ return false;
+ }
+
+ nsTArray<nsCString> keys;
+ {
+ StaticMutexAutoLock locker(gTelemetryHistogramMutex);
+ MOZ_ASSERT(internal_IsHistogramEnumId(id));
+
+ // This is not good standard behavior given that we have histogram instances
+ // covering multiple processes.
+ // However, changing this requires some broader changes to callers.
+ KeyedHistogram* keyed =
+ internal_GetKeyedHistogramById(id, ProcessID::Parent);
+
+ MOZ_ASSERT(keyed);
+ if (!keyed) {
+ return false;
+ }
+
+ if (NS_FAILED(
+ keyed->GetKeys(locker, NS_ConvertUTF16toUTF8(storeName), keys))) {
+ return false;
+ }
+ }
+
+ // Convert keys from nsTArray<nsCString> to JS array.
+ JS::RootedVector<JS::Value> autoKeys(cx);
+ if (!autoKeys.reserve(keys.Length())) {
+ return false;
+ }
+
+ for (const auto& key : keys) {
+ JS::Rooted<JS::Value> jsKey(cx);
+ jsKey.setString(ToJSString(cx, key));
+ if (!autoKeys.append(jsKey)) {
+ return false;
+ }
+ }
+
+ JS::Rooted<JSObject*> jsKeys(cx, JS::NewArrayObject(cx, autoKeys));
+ if (!jsKeys) {
+ return false;
+ }
+
+ args.rval().setObject(*jsKeys);
+ return true;
+}
+
+bool internal_JSKeyedHistogram_Clear(JSContext* cx, unsigned argc,
+ JS::Value* vp) {
+ if (!XRE_IsParentProcess()) {
+ JS_ReportErrorASCII(
+ cx, "Keyed histograms can only be cleared in the parent process");
+ return false;
+ }
+
+ JS::CallArgs args = JS::CallArgsFromVp(argc, vp);
+
+ if (!args.thisv().isObject() ||
+ JS::GetClass(&args.thisv().toObject()) != &sJSKeyedHistogramClass) {
+ JS_ReportErrorASCII(cx, "Wrong JS class, expected JSKeyedHistogram class");
+ return false;
+ }
+
+ JSObject* obj = &args.thisv().toObject();
+ JSHistogramData* data = GetJSKeyedHistogramData(obj);
+ MOZ_ASSERT(data);
+ HistogramID id = data->histogramId;
+
+ // This function should always return |undefined| and never fail but
+ // rather report failures using the console.
+ args.rval().setUndefined();
+
+ nsAutoString storeName;
+ nsresult rv = internal_JS_StoreFromObjectArgument(cx, args, storeName);
+ if (NS_FAILED(rv)) {
+ return false;
+ }
+
+ KeyedHistogram* keyed = nullptr;
+ {
+ MOZ_ASSERT(internal_IsHistogramEnumId(id));
+ StaticMutexAutoLock locker(gTelemetryHistogramMutex);
+
+ // This is not good standard behavior given that we have histogram instances
+ // covering multiple processes.
+ // However, changing this requires some broader changes to callers.
+ keyed = internal_GetKeyedHistogramById(id, ProcessID::Parent,
+ /* instantiate = */ false);
+
+ if (!keyed) {
+ return true;
+ }
+
+ keyed->Clear(NS_ConvertUTF16toUTF8(storeName));
+ }
+
+ return true;
+}
+
+// NOTE: Runs without protection from |gTelemetryHistogramMutex|.
+// See comment at the top of this section.
+nsresult internal_WrapAndReturnKeyedHistogram(
+ HistogramID id, JSContext* cx, JS::MutableHandle<JS::Value> ret) {
+ JS::Rooted<JSObject*> obj(cx, JS_NewObject(cx, &sJSKeyedHistogramClass));
+ if (!obj) return NS_ERROR_FAILURE;
+ // The 6 functions that are wrapped up here are eventually called
+ // by the same thread that runs this function.
+ if (!(JS_DefineFunction(cx, obj, "add", internal_JSKeyedHistogram_Add, 2,
+ 0) &&
+ JS_DefineFunction(cx, obj, "name", internal_JSKeyedHistogram_Name, 1,
+ 0) &&
+ JS_DefineFunction(cx, obj, "snapshot",
+ internal_JSKeyedHistogram_Snapshot, 1, 0) &&
+ JS_DefineFunction(cx, obj, "keys", internal_JSKeyedHistogram_Keys, 1,
+ 0) &&
+ JS_DefineFunction(cx, obj, "clear", internal_JSKeyedHistogram_Clear, 1,
+ 0))) {
+ return NS_ERROR_FAILURE;
+ }
+
+ JSHistogramData* data = new JSHistogramData{id};
+ JS::SetReservedSlot(obj, HistogramObjectDataSlot, JS::PrivateValue(data));
+ ret.setObject(*obj);
+
+ return NS_OK;
+}
+
+void internal_JSKeyedHistogram_finalize(JS::GCContext* gcx, JSObject* obj) {
+ if (!obj || JS::GetClass(obj) != &sJSKeyedHistogramClass) {
+ MOZ_ASSERT_UNREACHABLE("Should have the right JS class.");
+ return;
+ }
+
+ JSHistogramData* data = GetJSKeyedHistogramData(obj);
+ MOZ_ASSERT(data);
+ delete data;
+}
+
+} // namespace
+
+////////////////////////////////////////////////////////////////////////
+////////////////////////////////////////////////////////////////////////
+//
+// EXTERNALLY VISIBLE FUNCTIONS in namespace TelemetryHistogram::
+
+// All of these functions are actually in namespace TelemetryHistogram::,
+// but the ::TelemetryHistogram prefix is given explicitly. This is
+// because it is critical to see which calls from these functions are
+// to another function in this interface. Mis-identifying "inwards
+// calls" from "calls to another function in this interface" will lead
+// to deadlocking and/or races. See comments at the top of the file
+// for further (important!) details.
+
+void TelemetryHistogram::InitializeGlobalState(bool canRecordBase,
+ bool canRecordExtended) {
+ StaticMutexAutoLock locker(gTelemetryHistogramMutex);
+ MOZ_ASSERT(!gInitDone,
+ "TelemetryHistogram::InitializeGlobalState "
+ "may only be called once");
+
+ gCanRecordBase = canRecordBase;
+ gCanRecordExtended = canRecordExtended;
+
+ if (XRE_IsParentProcess()) {
+ gHistogramStorage =
+ new Histogram* [HistogramCount * size_t(ProcessID::Count)] {};
+ gKeyedHistogramStorage =
+ new KeyedHistogram* [HistogramCount * size_t(ProcessID::Count)] {};
+ }
+
+ // Some Telemetry histograms depend on the value of C++ constants and hardcode
+ // their values in Histograms.json.
+ // We add static asserts here for those values to match so that future changes
+ // don't go unnoticed.
+ // clang-format off
+ static_assert((uint32_t(JS::GCReason::NUM_TELEMETRY_REASONS) + 1) ==
+ gHistogramInfos[mozilla::Telemetry::GC_MINOR_REASON].bucketCount &&
+ (uint32_t(JS::GCReason::NUM_TELEMETRY_REASONS) + 1) ==
+ gHistogramInfos[mozilla::Telemetry::GC_MINOR_REASON_LONG].bucketCount &&
+ (uint32_t(JS::GCReason::NUM_TELEMETRY_REASONS) + 1) ==
+ gHistogramInfos[mozilla::Telemetry::GC_REASON_2].bucketCount,
+ "NUM_TELEMETRY_REASONS is assumed to be a fixed value in Histograms.json."
+ " If this was an intentional change, update the n_values for the "
+ "following in Histograms.json: GC_MINOR_REASON, GC_MINOR_REASON_LONG, "
+ "GC_REASON_2");
+
+ // clang-format on
+
+ gInitDone = true;
+}
+
+void TelemetryHistogram::DeInitializeGlobalState() {
+ StaticMutexAutoLock locker(gTelemetryHistogramMutex);
+ gCanRecordBase = false;
+ gCanRecordExtended = false;
+ gInitDone = false;
+
+ // FactoryGet `new`s Histograms for us, but requires us to manually delete.
+ if (XRE_IsParentProcess()) {
+ for (size_t i = 0; i < HistogramCount * size_t(ProcessID::Count); ++i) {
+ if (gKeyedHistogramStorage[i] != gExpiredKeyedHistogram) {
+ delete gKeyedHistogramStorage[i];
+ }
+ if (gHistogramStorage[i] != gExpiredHistogram) {
+ delete gHistogramStorage[i];
+ }
+ }
+ delete[] gHistogramStorage;
+ delete[] gKeyedHistogramStorage;
+ }
+ delete gExpiredHistogram;
+ gExpiredHistogram = nullptr;
+ delete gExpiredKeyedHistogram;
+ gExpiredKeyedHistogram = nullptr;
+}
+
+#ifdef DEBUG
+bool TelemetryHistogram::GlobalStateHasBeenInitialized() {
+ StaticMutexAutoLock locker(gTelemetryHistogramMutex);
+ return gInitDone;
+}
+#endif
+
+bool TelemetryHistogram::CanRecordBase() {
+ StaticMutexAutoLock locker(gTelemetryHistogramMutex);
+ return internal_CanRecordBase();
+}
+
+void TelemetryHistogram::SetCanRecordBase(bool b) {
+ StaticMutexAutoLock locker(gTelemetryHistogramMutex);
+ gCanRecordBase = b;
+}
+
+bool TelemetryHistogram::CanRecordExtended() {
+ StaticMutexAutoLock locker(gTelemetryHistogramMutex);
+ return internal_CanRecordExtended();
+}
+
+void TelemetryHistogram::SetCanRecordExtended(bool b) {
+ StaticMutexAutoLock locker(gTelemetryHistogramMutex);
+ gCanRecordExtended = b;
+}
+
+void TelemetryHistogram::InitHistogramRecordingEnabled() {
+ StaticMutexAutoLock locker(gTelemetryHistogramMutex);
+ auto processType = XRE_GetProcessType();
+ for (size_t i = 0; i < HistogramCount; ++i) {
+ const HistogramInfo& h = gHistogramInfos[i];
+ mozilla::Telemetry::HistogramID id = mozilla::Telemetry::HistogramID(i);
+ bool canRecordInProcess =
+ CanRecordInProcess(h.record_in_processes, processType);
+ internal_SetHistogramRecordingEnabled(locker, id, canRecordInProcess);
+ }
+
+ for (auto recordingInitiallyDisabledID : kRecordingInitiallyDisabledIDs) {
+ internal_SetHistogramRecordingEnabled(locker, recordingInitiallyDisabledID,
+ false);
+ }
+}
+
+void TelemetryHistogram::SetHistogramRecordingEnabled(HistogramID aID,
+ bool aEnabled) {
+ if (NS_WARN_IF(!internal_IsHistogramEnumId(aID))) {
+ MOZ_ASSERT_UNREACHABLE("Histogram usage requires valid ids.");
+ return;
+ }
+
+ const HistogramInfo& h = gHistogramInfos[aID];
+ if (!CanRecordInProcess(h.record_in_processes, XRE_GetProcessType())) {
+ // Don't permit record_in_process-disabled recording to be re-enabled.
+ return;
+ }
+
+ if (!CanRecordProduct(h.products)) {
+ // Don't permit products-disabled recording to be re-enabled.
+ return;
+ }
+
+ StaticMutexAutoLock locker(gTelemetryHistogramMutex);
+ internal_SetHistogramRecordingEnabled(locker, aID, aEnabled);
+}
+
+nsresult TelemetryHistogram::SetHistogramRecordingEnabled(
+ const nsACString& name, bool aEnabled) {
+ StaticMutexAutoLock locker(gTelemetryHistogramMutex);
+ HistogramID id;
+ if (NS_FAILED(internal_GetHistogramIdByName(locker, name, &id))) {
+ return NS_ERROR_FAILURE;
+ }
+
+ const HistogramInfo& hi = gHistogramInfos[id];
+ if (CanRecordInProcess(hi.record_in_processes, XRE_GetProcessType())) {
+ internal_SetHistogramRecordingEnabled(locker, id, aEnabled);
+ }
+ return NS_OK;
+}
+
+void TelemetryHistogram::Accumulate(HistogramID aID, uint32_t aSample) {
+ if (NS_WARN_IF(!internal_IsHistogramEnumId(aID))) {
+ MOZ_ASSERT_UNREACHABLE("Histogram usage requires valid ids.");
+ return;
+ }
+
+ StaticMutexAutoLock locker(gTelemetryHistogramMutex);
+ internal_Accumulate(locker, aID, aSample);
+}
+
+void TelemetryHistogram::Accumulate(HistogramID aID,
+ const nsTArray<uint32_t>& aSamples) {
+ if (NS_WARN_IF(!internal_IsHistogramEnumId(aID))) {
+ MOZ_ASSERT_UNREACHABLE("Histogram usage requires valid ids.");
+ return;
+ }
+
+ MOZ_ASSERT(!gHistogramInfos[aID].keyed,
+ "Cannot accumulate into a keyed histogram. No key given.");
+
+ StaticMutexAutoLock locker(gTelemetryHistogramMutex);
+ for (uint32_t sample : aSamples) {
+ internal_Accumulate(locker, aID, sample);
+ }
+}
+
+void TelemetryHistogram::Accumulate(HistogramID aID, const nsCString& aKey,
+ uint32_t aSample) {
+ if (NS_WARN_IF(!internal_IsHistogramEnumId(aID))) {
+ MOZ_ASSERT_UNREACHABLE("Histogram usage requires valid ids.");
+ return;
+ }
+
+ // Check if we're allowed to record in the provided key, for this histogram.
+ if (!gHistogramInfos[aID].allows_key(aKey)) {
+ nsPrintfCString msg("%s - key '%s' not allowed for this keyed histogram",
+ gHistogramInfos[aID].name(), aKey.get());
+ LogToBrowserConsole(nsIScriptError::errorFlag, NS_ConvertUTF8toUTF16(msg));
+ TelemetryScalar::Add(mozilla::Telemetry::ScalarID::
+ TELEMETRY_ACCUMULATE_UNKNOWN_HISTOGRAM_KEYS,
+ NS_ConvertASCIItoUTF16(gHistogramInfos[aID].name()),
+ 1);
+ return;
+ }
+
+ StaticMutexAutoLock locker(gTelemetryHistogramMutex);
+ internal_Accumulate(locker, aID, aKey, aSample);
+}
+
+void TelemetryHistogram::Accumulate(HistogramID aID, const nsCString& aKey,
+ const nsTArray<uint32_t>& aSamples) {
+ if (NS_WARN_IF(!internal_IsHistogramEnumId(aID))) {
+ MOZ_ASSERT_UNREACHABLE("Histogram usage requires valid ids");
+ return;
+ }
+
+ // Check that this histogram is keyed
+ MOZ_ASSERT(gHistogramInfos[aID].keyed,
+ "Cannot accumulate into a non-keyed histogram using a key.");
+
+ // Check if we're allowed to record in the provided key, for this histogram.
+ if (!gHistogramInfos[aID].allows_key(aKey)) {
+ nsPrintfCString msg("%s - key '%s' not allowed for this keyed histogram",
+ gHistogramInfos[aID].name(), aKey.get());
+ LogToBrowserConsole(nsIScriptError::errorFlag, NS_ConvertUTF8toUTF16(msg));
+ TelemetryScalar::Add(mozilla::Telemetry::ScalarID::
+ TELEMETRY_ACCUMULATE_UNKNOWN_HISTOGRAM_KEYS,
+ NS_ConvertASCIItoUTF16(gHistogramInfos[aID].name()),
+ 1);
+ return;
+ }
+
+ StaticMutexAutoLock locker(gTelemetryHistogramMutex);
+ for (uint32_t sample : aSamples) {
+ internal_Accumulate(locker, aID, aKey, sample);
+ }
+}
+
+nsresult TelemetryHistogram::Accumulate(const char* name, uint32_t sample) {
+ StaticMutexAutoLock locker(gTelemetryHistogramMutex);
+ if (!internal_CanRecordBase()) {
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+ HistogramID id;
+ nsresult rv =
+ internal_GetHistogramIdByName(locker, nsDependentCString(name), &id);
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+ internal_Accumulate(locker, id, sample);
+ return NS_OK;
+}
+
+nsresult TelemetryHistogram::Accumulate(const char* name, const nsCString& key,
+ uint32_t sample) {
+ bool keyNotAllowed = false;
+
+ {
+ StaticMutexAutoLock locker(gTelemetryHistogramMutex);
+ if (!internal_CanRecordBase()) {
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+ HistogramID id;
+ nsresult rv =
+ internal_GetHistogramIdByName(locker, nsDependentCString(name), &id);
+ if (NS_SUCCEEDED(rv)) {
+ // Check if we're allowed to record in the provided key, for this
+ // histogram.
+ if (gHistogramInfos[id].allows_key(key)) {
+ internal_Accumulate(locker, id, key, sample);
+ return NS_OK;
+ }
+ // We're holding |gTelemetryHistogramMutex|, so we can't print a message
+ // here.
+ keyNotAllowed = true;
+ }
+ }
+
+ if (keyNotAllowed) {
+ LogToBrowserConsole(nsIScriptError::errorFlag,
+ u"Key not allowed for this keyed histogram"_ns);
+ TelemetryScalar::Add(mozilla::Telemetry::ScalarID::
+ TELEMETRY_ACCUMULATE_UNKNOWN_HISTOGRAM_KEYS,
+ NS_ConvertASCIItoUTF16(name), 1);
+ }
+ return NS_ERROR_FAILURE;
+}
+
+void TelemetryHistogram::AccumulateCategorical(HistogramID aId,
+ const nsCString& label) {
+ if (NS_WARN_IF(!internal_IsHistogramEnumId(aId))) {
+ MOZ_ASSERT_UNREACHABLE("Histogram usage requires valid ids.");
+ return;
+ }
+
+ StaticMutexAutoLock locker(gTelemetryHistogramMutex);
+ if (!internal_CanRecordBase()) {
+ return;
+ }
+ uint32_t labelId = 0;
+ if (NS_FAILED(gHistogramInfos[aId].label_id(label.get(), &labelId))) {
+ return;
+ }
+ internal_Accumulate(locker, aId, labelId);
+}
+
+void TelemetryHistogram::AccumulateCategorical(
+ HistogramID aId, const nsTArray<nsCString>& aLabels) {
+ if (NS_WARN_IF(!internal_IsHistogramEnumId(aId))) {
+ MOZ_ASSERT_UNREACHABLE("Histogram usage requires valid ids.");
+ return;
+ }
+
+ if (!internal_CanRecordBase()) {
+ return;
+ }
+
+ // We use two loops, one for getting label_ids and another one for actually
+ // accumulating the values. This ensures that in the case of an invalid label
+ // in the array, no values are accumulated. In any call to this API, either
+ // all or (in case of error) none of the values will be accumulated.
+
+ nsTArray<uint32_t> intSamples(aLabels.Length());
+ for (const nsCString& label : aLabels) {
+ uint32_t labelId = 0;
+ if (NS_FAILED(gHistogramInfos[aId].label_id(label.get(), &labelId))) {
+ return;
+ }
+ intSamples.AppendElement(labelId);
+ }
+
+ StaticMutexAutoLock locker(gTelemetryHistogramMutex);
+
+ for (uint32_t sample : intSamples) {
+ internal_Accumulate(locker, aId, sample);
+ }
+}
+
+void TelemetryHistogram::AccumulateChild(
+ ProcessID aProcessType,
+ const nsTArray<HistogramAccumulation>& aAccumulations) {
+ MOZ_ASSERT(XRE_IsParentProcess());
+
+ StaticMutexAutoLock locker(gTelemetryHistogramMutex);
+ if (!internal_CanRecordBase()) {
+ return;
+ }
+ for (uint32_t i = 0; i < aAccumulations.Length(); ++i) {
+ if (NS_WARN_IF(!internal_IsHistogramEnumId(aAccumulations[i].mId))) {
+ MOZ_ASSERT_UNREACHABLE("Histogram usage requires valid ids.");
+ continue;
+ }
+ internal_AccumulateChild(locker, aProcessType, aAccumulations[i].mId,
+ aAccumulations[i].mSample);
+ }
+}
+
+void TelemetryHistogram::AccumulateChildKeyed(
+ ProcessID aProcessType,
+ const nsTArray<KeyedHistogramAccumulation>& aAccumulations) {
+ MOZ_ASSERT(XRE_IsParentProcess());
+ StaticMutexAutoLock locker(gTelemetryHistogramMutex);
+ if (!internal_CanRecordBase()) {
+ return;
+ }
+ for (uint32_t i = 0; i < aAccumulations.Length(); ++i) {
+ if (NS_WARN_IF(!internal_IsHistogramEnumId(aAccumulations[i].mId))) {
+ MOZ_ASSERT_UNREACHABLE("Histogram usage requires valid ids.");
+ continue;
+ }
+ internal_AccumulateChildKeyed(locker, aProcessType, aAccumulations[i].mId,
+ aAccumulations[i].mKey,
+ aAccumulations[i].mSample);
+ }
+}
+
+nsresult TelemetryHistogram::GetAllStores(StringHashSet& set) {
+ for (uint32_t storeIdx : gHistogramStoresTable) {
+ const char* name = &gHistogramStringTable[storeIdx];
+ nsAutoCString store;
+ store.AssignASCII(name);
+ if (!set.Insert(store, mozilla::fallible)) {
+ return NS_ERROR_FAILURE;
+ }
+ }
+ return NS_OK;
+}
+
+nsresult TelemetryHistogram::GetCategoricalHistogramLabels(
+ JSContext* aCx, JS::MutableHandle<JS::Value> aResult) {
+ JS::Rooted<JSObject*> root_obj(aCx, JS_NewPlainObject(aCx));
+ if (!root_obj) {
+ return NS_ERROR_FAILURE;
+ }
+ aResult.setObject(*root_obj);
+
+ for (const HistogramInfo& info : gHistogramInfos) {
+ if (info.histogramType != nsITelemetry::HISTOGRAM_CATEGORICAL) {
+ continue;
+ }
+
+ const char* name = info.name();
+ JS::Rooted<JSObject*> labels(aCx,
+ JS::NewArrayObject(aCx, info.label_count));
+ if (!labels) {
+ return NS_ERROR_FAILURE;
+ }
+
+ if (!JS_DefineProperty(aCx, root_obj, name, labels, JSPROP_ENUMERATE)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ for (uint32_t i = 0; i < info.label_count; ++i) {
+ uint32_t string_offset = gHistogramLabelTable[info.label_index + i];
+ const char* const label = &gHistogramStringTable[string_offset];
+ auto clabel = NS_ConvertASCIItoUTF16(label);
+ JS::Rooted<JS::Value> value(aCx);
+ value.setString(ToJSString(aCx, clabel));
+ if (!JS_DefineElement(aCx, labels, i, value, JSPROP_ENUMERATE)) {
+ return NS_ERROR_FAILURE;
+ }
+ }
+ }
+
+ return NS_OK;
+}
+
+nsresult TelemetryHistogram::GetHistogramById(
+ const nsACString& name, JSContext* cx, JS::MutableHandle<JS::Value> ret) {
+ HistogramID id;
+ {
+ StaticMutexAutoLock locker(gTelemetryHistogramMutex);
+ nsresult rv = internal_GetHistogramIdByName(locker, name, &id);
+ if (NS_FAILED(rv)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ if (gHistogramInfos[id].keyed) {
+ return NS_ERROR_FAILURE;
+ }
+ }
+ // Runs without protection from |gTelemetryHistogramMutex|
+ return internal_WrapAndReturnHistogram(id, cx, ret);
+}
+
+nsresult TelemetryHistogram::GetKeyedHistogramById(
+ const nsACString& name, JSContext* cx, JS::MutableHandle<JS::Value> ret) {
+ HistogramID id;
+ {
+ StaticMutexAutoLock locker(gTelemetryHistogramMutex);
+ nsresult rv = internal_GetHistogramIdByName(locker, name, &id);
+ if (NS_FAILED(rv)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ if (!gHistogramInfos[id].keyed) {
+ return NS_ERROR_FAILURE;
+ }
+ }
+ // Runs without protection from |gTelemetryHistogramMutex|
+ return internal_WrapAndReturnKeyedHistogram(id, cx, ret);
+}
+
+const char* TelemetryHistogram::GetHistogramName(HistogramID id) {
+ if (NS_WARN_IF(!internal_IsHistogramEnumId(id))) {
+ MOZ_ASSERT_UNREACHABLE("Histogram usage requires valid ids.");
+ return nullptr;
+ }
+
+ StaticMutexAutoLock locker(gTelemetryHistogramMutex);
+ const HistogramInfo& h = gHistogramInfos[id];
+ return h.name();
+}
+
+nsresult TelemetryHistogram::CreateHistogramSnapshots(
+ JSContext* aCx, JS::MutableHandle<JS::Value> aResult,
+ const nsACString& aStore, unsigned int aDataset, bool aClearSubsession,
+ bool aFilterTest) {
+ if (!XRE_IsParentProcess()) {
+ return NS_ERROR_FAILURE;
+ }
+
+ // Runs without protection from |gTelemetryHistogramMutex|
+ JS::Rooted<JSObject*> root_obj(aCx, JS_NewPlainObject(aCx));
+ if (!root_obj) {
+ return NS_ERROR_FAILURE;
+ }
+ aResult.setObject(*root_obj);
+
+ // Include the GPU process in histogram snapshots only if we actually tried
+ // to launch a process for it.
+ bool includeGPUProcess = internal_AttemptedGPUProcess();
+
+ HistogramProcessSnapshotsArray processHistArray;
+ {
+ StaticMutexAutoLock locker(gTelemetryHistogramMutex);
+ nsresult rv = internal_GetHistogramsSnapshot(
+ locker, aStore, aDataset, aClearSubsession, includeGPUProcess,
+ aFilterTest, processHistArray);
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+ }
+
+ // Make the JS calls on the stashed histograms for every process
+ for (uint32_t process = 0; process < processHistArray.length(); ++process) {
+ JS::Rooted<JSObject*> processObject(aCx, JS_NewPlainObject(aCx));
+ if (!processObject) {
+ return NS_ERROR_FAILURE;
+ }
+ if (!JS_DefineProperty(aCx, root_obj,
+ GetNameForProcessID(ProcessID(process)),
+ processObject, JSPROP_ENUMERATE)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ for (const HistogramSnapshotInfo& hData : processHistArray[process]) {
+ HistogramID id = hData.histogramID;
+
+ JS::Rooted<JSObject*> hobj(aCx, JS_NewPlainObject(aCx));
+ if (!hobj) {
+ return NS_ERROR_FAILURE;
+ }
+
+ if (NS_FAILED(internal_ReflectHistogramAndSamples(
+ aCx, hobj, gHistogramInfos[id], hData.data))) {
+ return NS_ERROR_FAILURE;
+ }
+
+ if (!JS_DefineProperty(aCx, processObject, gHistogramInfos[id].name(),
+ hobj, JSPROP_ENUMERATE)) {
+ return NS_ERROR_FAILURE;
+ }
+ }
+ }
+ return NS_OK;
+}
+
+nsresult TelemetryHistogram::GetKeyedHistogramSnapshots(
+ JSContext* aCx, JS::MutableHandle<JS::Value> aResult,
+ const nsACString& aStore, unsigned int aDataset, bool aClearSubsession,
+ bool aFilterTest) {
+ if (!XRE_IsParentProcess()) {
+ return NS_ERROR_FAILURE;
+ }
+
+ // Runs without protection from |gTelemetryHistogramMutex|
+ JS::Rooted<JSObject*> obj(aCx, JS_NewPlainObject(aCx));
+ if (!obj) {
+ return NS_ERROR_FAILURE;
+ }
+ aResult.setObject(*obj);
+
+ // Include the GPU process in histogram snapshots only if we actually tried
+ // to launch a process for it.
+ bool includeGPUProcess = internal_AttemptedGPUProcess();
+
+ // Get a snapshot of all the data while holding the mutex.
+ KeyedHistogramProcessSnapshotsArray processHistArray;
+ {
+ StaticMutexAutoLock locker(gTelemetryHistogramMutex);
+ nsresult rv = internal_GetKeyedHistogramsSnapshot(
+ locker, aStore, aDataset, aClearSubsession, includeGPUProcess,
+ aFilterTest, processHistArray);
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+ }
+
+ // Mirror the snapshot data to JS, now that we released the mutex.
+ for (uint32_t process = 0; process < processHistArray.length(); ++process) {
+ JS::Rooted<JSObject*> processObject(aCx, JS_NewPlainObject(aCx));
+ if (!processObject) {
+ return NS_ERROR_FAILURE;
+ }
+ if (!JS_DefineProperty(aCx, obj, GetNameForProcessID(ProcessID(process)),
+ processObject, JSPROP_ENUMERATE)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ for (const KeyedHistogramSnapshotInfo& hData : processHistArray[process]) {
+ const HistogramInfo& info = gHistogramInfos[hData.histogramId];
+
+ JS::Rooted<JSObject*> snapshot(aCx, JS_NewPlainObject(aCx));
+ if (!snapshot) {
+ return NS_ERROR_FAILURE;
+ }
+
+ if (!NS_SUCCEEDED(internal_ReflectKeyedHistogram(hData.data, info, aCx,
+ snapshot))) {
+ return NS_ERROR_FAILURE;
+ }
+
+ if (!JS_DefineProperty(aCx, processObject, info.name(), snapshot,
+ JSPROP_ENUMERATE)) {
+ return NS_ERROR_FAILURE;
+ }
+ }
+ }
+ return NS_OK;
+}
+
+size_t TelemetryHistogram::GetHistogramSizesOfIncludingThis(
+ mozilla::MallocSizeOf aMallocSizeOf) {
+ StaticMutexAutoLock locker(gTelemetryHistogramMutex);
+
+ size_t n = 0;
+
+ // If we allocated the array, let's count the number of pointers in there and
+ // each entry's size.
+ if (gKeyedHistogramStorage) {
+ n += HistogramCount * size_t(ProcessID::Count) * sizeof(KeyedHistogram*);
+ for (size_t i = 0; i < HistogramCount * size_t(ProcessID::Count); ++i) {
+ if (gKeyedHistogramStorage[i] &&
+ gKeyedHistogramStorage[i] != gExpiredKeyedHistogram) {
+ n += gKeyedHistogramStorage[i]->SizeOfIncludingThis(aMallocSizeOf);
+ }
+ }
+ }
+
+ // If we allocated the array, let's count the number of pointers in there.
+ if (gHistogramStorage) {
+ n += HistogramCount * size_t(ProcessID::Count) * sizeof(Histogram*);
+ for (size_t i = 0; i < HistogramCount * size_t(ProcessID::Count); ++i) {
+ if (gHistogramStorage[i] && gHistogramStorage[i] != gExpiredHistogram) {
+ n += gHistogramStorage[i]->SizeOfIncludingThis(aMallocSizeOf);
+ }
+ }
+ }
+
+ // We only allocate the expired (keyed) histogram once.
+ if (gExpiredKeyedHistogram) {
+ n += gExpiredKeyedHistogram->SizeOfIncludingThis(aMallocSizeOf);
+ }
+
+ if (gExpiredHistogram) {
+ n += gExpiredHistogram->SizeOfIncludingThis(aMallocSizeOf);
+ }
+
+ return n;
+}
+
+////////////////////////////////////////////////////////////////////////
+////////////////////////////////////////////////////////////////////////
+//
+// PRIVATE: GeckoView specific helpers
+
+namespace base {
+class PersistedSampleSet : public base::Histogram::SampleSet {
+ public:
+ explicit PersistedSampleSet(const nsTArray<base::Histogram::Count>& aCounts,
+ int64_t aSampleSum);
+};
+
+PersistedSampleSet::PersistedSampleSet(
+ const nsTArray<base::Histogram::Count>& aCounts, int64_t aSampleSum) {
+ // Initialize the data in the base class. See Histogram::SampleSet
+ // for the fields documentation.
+ const size_t numCounts = aCounts.Length();
+ counts_.SetLength(numCounts);
+
+ for (size_t i = 0; i < numCounts; i++) {
+ counts_[i] = aCounts[i];
+ redundant_count_ += aCounts[i];
+ }
+ sum_ = aSampleSum;
+};
+} // namespace base
+
+namespace {
+/**
+ * Helper function to write histogram properties to JSON.
+ * Please note that this needs to be called between
+ * StartObjectProperty/EndObject calls that mark the histogram's
+ * JSON creation.
+ */
+void internal_ReflectHistogramToJSON(const HistogramSnapshotData& aSnapshot,
+ mozilla::JSONWriter& aWriter) {
+ aWriter.IntProperty("sum", aSnapshot.mSampleSum);
+
+ // Fill the "counts" property.
+ aWriter.StartArrayProperty("counts");
+ for (size_t i = 0; i < aSnapshot.mBucketCounts.Length(); i++) {
+ aWriter.IntElement(aSnapshot.mBucketCounts[i]);
+ }
+ aWriter.EndArray();
+}
+
+bool internal_CanRecordHistogram(const HistogramID id, ProcessID aProcessType) {
+ // Check if we are allowed to record the data.
+ if (!CanRecordDataset(gHistogramInfos[id].dataset, internal_CanRecordBase(),
+ internal_CanRecordExtended())) {
+ return false;
+ }
+
+ // Check if we're allowed to record in the given process.
+ if (aProcessType == ProcessID::Parent && !internal_IsRecordingEnabled(id)) {
+ return false;
+ }
+
+ if (aProcessType != ProcessID::Parent &&
+ !CanRecordInProcess(gHistogramInfos[id].record_in_processes,
+ aProcessType)) {
+ return false;
+ }
+
+ // Don't record if the current platform is not enabled
+ if (!CanRecordProduct(gHistogramInfos[id].products)) {
+ return false;
+ }
+
+ return true;
+}
+
+nsresult internal_ParseHistogramData(
+ JSContext* aCx, JS::Handle<JS::PropertyKey> aEntryId,
+ JS::Handle<JSObject*> aContainerObj, nsACString& aOutName,
+ nsTArray<base::Histogram::Count>& aOutCountArray, int64_t& aOutSum) {
+ // Get the histogram name.
+ nsAutoJSString histogramName;
+ if (!histogramName.init(aCx, aEntryId)) {
+ JS_ClearPendingException(aCx);
+ return NS_ERROR_FAILURE;
+ }
+
+ CopyUTF16toUTF8(histogramName, aOutName);
+
+ // Get the data for this histogram.
+ JS::Rooted<JS::Value> histogramData(aCx);
+ if (!JS_GetPropertyById(aCx, aContainerObj, aEntryId, &histogramData)) {
+ JS_ClearPendingException(aCx);
+ return NS_ERROR_FAILURE;
+ }
+
+ if (!histogramData.isObject()) {
+ // base::Histogram data need to be an object. If that's not the case, skip
+ // it and try to load the rest of the data.
+ return NS_ERROR_FAILURE;
+ }
+
+ // Get the "sum" property.
+ JS::Rooted<JS::Value> sumValue(aCx);
+ JS::Rooted<JSObject*> histogramObj(aCx, &histogramData.toObject());
+ if (!JS_GetProperty(aCx, histogramObj, "sum", &sumValue)) {
+ JS_ClearPendingException(aCx);
+ return NS_ERROR_FAILURE;
+ }
+
+ if (!JS::ToInt64(aCx, sumValue, &aOutSum)) {
+ JS_ClearPendingException(aCx);
+ return NS_ERROR_FAILURE;
+ }
+
+ // Get the "counts" array.
+ JS::Rooted<JS::Value> countsArray(aCx);
+ bool countsIsArray = false;
+ if (!JS_GetProperty(aCx, histogramObj, "counts", &countsArray) ||
+ !JS::IsArrayObject(aCx, countsArray, &countsIsArray)) {
+ JS_ClearPendingException(aCx);
+ return NS_ERROR_FAILURE;
+ }
+
+ if (!countsIsArray) {
+ // The "counts" property needs to be an array. If this is not the case,
+ // skip this histogram.
+ return NS_ERROR_FAILURE;
+ }
+
+ // Get the length of the array.
+ uint32_t countsLen = 0;
+ JS::Rooted<JSObject*> countsArrayObj(aCx, &countsArray.toObject());
+ if (!JS::GetArrayLength(aCx, countsArrayObj, &countsLen)) {
+ JS_ClearPendingException(aCx);
+ return NS_ERROR_FAILURE;
+ }
+
+ // Parse the "counts" in the array.
+ for (uint32_t arrayIdx = 0; arrayIdx < countsLen; arrayIdx++) {
+ JS::Rooted<JS::Value> elementValue(aCx);
+ int countAsInt = 0;
+ if (!JS_GetElement(aCx, countsArrayObj, arrayIdx, &elementValue) ||
+ !JS::ToInt32(aCx, elementValue, &countAsInt)) {
+ JS_ClearPendingException(aCx);
+ return NS_ERROR_FAILURE;
+ }
+ aOutCountArray.AppendElement(countAsInt);
+ }
+
+ return NS_OK;
+}
+
+} // Anonymous namespace
+
+////////////////////////////////////////////////////////////////////////
+////////////////////////////////////////////////////////////////////////
+//
+// PUBLIC: GeckoView serialization/deserialization functions.
+
+nsresult TelemetryHistogram::SerializeHistograms(mozilla::JSONWriter& aWriter) {
+ MOZ_ASSERT(XRE_IsParentProcess(),
+ "Only save histograms in the parent process");
+ if (!XRE_IsParentProcess()) {
+ return NS_ERROR_FAILURE;
+ }
+
+ // Include the GPU process in histogram snapshots only if we actually tried
+ // to launch a process for it.
+ bool includeGPUProcess = internal_AttemptedGPUProcess();
+
+ // Take a snapshot of the histograms.
+ HistogramProcessSnapshotsArray processHistArray;
+ {
+ StaticMutexAutoLock locker(gTelemetryHistogramMutex);
+ // We always request the "opt-in"/"prerelease" dataset: we internally
+ // record the right subset, so this will only return "prerelease" if
+ // it was recorded.
+ if (NS_FAILED(internal_GetHistogramsSnapshot(
+ locker, "main"_ns, nsITelemetry::DATASET_PRERELEASE_CHANNELS,
+ false /* aClearSubsession */, includeGPUProcess,
+ false /* aFilterTest */, processHistArray))) {
+ return NS_ERROR_FAILURE;
+ }
+ }
+
+ // Make the JSON calls on the stashed histograms for every process
+ for (uint32_t process = 0; process < processHistArray.length(); ++process) {
+ aWriter.StartObjectProperty(
+ mozilla::MakeStringSpan(GetNameForProcessID(ProcessID(process))));
+
+ for (const HistogramSnapshotInfo& hData : processHistArray[process]) {
+ HistogramID id = hData.histogramID;
+
+ aWriter.StartObjectProperty(
+ mozilla::MakeStringSpan(gHistogramInfos[id].name()));
+ internal_ReflectHistogramToJSON(hData.data, aWriter);
+ aWriter.EndObject();
+ }
+ aWriter.EndObject();
+ }
+
+ return NS_OK;
+}
+
+nsresult TelemetryHistogram::SerializeKeyedHistograms(
+ mozilla::JSONWriter& aWriter) {
+ MOZ_ASSERT(XRE_IsParentProcess(),
+ "Only save keyed histograms in the parent process");
+ if (!XRE_IsParentProcess()) {
+ return NS_ERROR_FAILURE;
+ }
+
+ // Include the GPU process in histogram snapshots only if we actually tried
+ // to launch a process for it.
+ bool includeGPUProcess = internal_AttemptedGPUProcess();
+
+ // Take a snapshot of the keyed histograms.
+ KeyedHistogramProcessSnapshotsArray processHistArray;
+ {
+ StaticMutexAutoLock locker(gTelemetryHistogramMutex);
+ // We always request the "opt-in"/"prerelease" dataset: we internally
+ // record the right subset, so this will only return "prerelease" if
+ // it was recorded.
+ if (NS_FAILED(internal_GetKeyedHistogramsSnapshot(
+ locker, "main"_ns, nsITelemetry::DATASET_PRERELEASE_CHANNELS,
+ false /* aClearSubsession */, includeGPUProcess,
+ false /* aFilterTest */, processHistArray))) {
+ return NS_ERROR_FAILURE;
+ }
+ }
+
+ // Serialize the keyed histograms for every process.
+ for (uint32_t process = 0; process < processHistArray.length(); ++process) {
+ aWriter.StartObjectProperty(
+ mozilla::MakeStringSpan(GetNameForProcessID(ProcessID(process))));
+
+ const KeyedHistogramSnapshotsArray& hArray = processHistArray[process];
+ for (size_t i = 0; i < hArray.length(); ++i) {
+ const KeyedHistogramSnapshotInfo& hData = hArray[i];
+ HistogramID id = hData.histogramId;
+ const HistogramInfo& info = gHistogramInfos[id];
+
+ aWriter.StartObjectProperty(mozilla::MakeStringSpan(info.name()));
+
+ // Each key is a new object with a "sum" and a "counts" property.
+ for (const auto& entry : hData.data) {
+ const HistogramSnapshotData& keyData = entry.GetData();
+ aWriter.StartObjectProperty(PromiseFlatCString(entry.GetKey()));
+ internal_ReflectHistogramToJSON(keyData, aWriter);
+ aWriter.EndObject();
+ }
+
+ aWriter.EndObject();
+ }
+ aWriter.EndObject();
+ }
+
+ return NS_OK;
+}
+
+nsresult TelemetryHistogram::DeserializeHistograms(
+ JSContext* aCx, JS::Handle<JS::Value> aData) {
+ MOZ_ASSERT(XRE_IsParentProcess(),
+ "Only load histograms in the parent process");
+ if (!XRE_IsParentProcess()) {
+ return NS_ERROR_FAILURE;
+ }
+
+ // Telemetry is disabled. This should never happen, but let's leave this check
+ // for consistency with other histogram updates routines.
+ if (!internal_CanRecordBase()) {
+ return NS_OK;
+ }
+
+ typedef std::tuple<nsCString, nsTArray<base::Histogram::Count>, int64_t>
+ PersistedHistogramTuple;
+ typedef mozilla::Vector<PersistedHistogramTuple> PersistedHistogramArray;
+ typedef mozilla::Vector<PersistedHistogramArray> PersistedHistogramStorage;
+
+ // Before updating the histograms, we need to get the data out of the JS
+ // wrappers. We can't hold the histogram mutex while handling JS stuff.
+ // Build a <histogram name, value> map.
+ JS::Rooted<JSObject*> histogramDataObj(aCx, &aData.toObject());
+ JS::Rooted<JS::IdVector> processes(aCx, JS::IdVector(aCx));
+ if (!JS_Enumerate(aCx, histogramDataObj, &processes)) {
+ // We can't even enumerate the processes in the loaded data, so
+ // there is nothing we could recover from the persistence file. Bail out.
+ JS_ClearPendingException(aCx);
+ return NS_ERROR_FAILURE;
+ }
+
+ // Make sure we have enough storage for all the processes.
+ PersistedHistogramStorage histogramsToUpdate;
+ if (!histogramsToUpdate.resize(static_cast<uint32_t>(ProcessID::Count))) {
+ return NS_ERROR_OUT_OF_MEMORY;
+ }
+
+ // The following block of code attempts to extract as much data as possible
+ // from the serialized JSON, even in case of light data corruptions: if, for
+ // example, the data for a single process is corrupted or is in an unexpected
+ // form, we press on and attempt to load the data for the other processes.
+ JS::Rooted<JS::PropertyKey> process(aCx);
+ for (auto& processVal : processes) {
+ // This is required as JS API calls require an Handle<jsid> and not a
+ // plain jsid.
+ process = processVal;
+ // Get the process name.
+ nsAutoJSString processNameJS;
+ if (!processNameJS.init(aCx, process)) {
+ JS_ClearPendingException(aCx);
+ continue;
+ }
+
+ // Make sure it's valid. Note that this is safe to call outside
+ // of a locked section.
+ NS_ConvertUTF16toUTF8 processName(processNameJS);
+ ProcessID processID = GetIDForProcessName(processName.get());
+ if (processID == ProcessID::Count) {
+ NS_WARNING(
+ nsPrintfCString("Failed to get process ID for %s", processName.get())
+ .get());
+ continue;
+ }
+
+ // And its probes.
+ JS::Rooted<JS::Value> processData(aCx);
+ if (!JS_GetPropertyById(aCx, histogramDataObj, process, &processData)) {
+ JS_ClearPendingException(aCx);
+ continue;
+ }
+
+ if (!processData.isObject()) {
+ // |processData| should be an object containing histograms. If this is
+ // not the case, silently skip and try to load the data for the other
+ // processes.
+ continue;
+ }
+
+ // Iterate through each histogram.
+ JS::Rooted<JSObject*> processDataObj(aCx, &processData.toObject());
+ JS::Rooted<JS::IdVector> histograms(aCx, JS::IdVector(aCx));
+ if (!JS_Enumerate(aCx, processDataObj, &histograms)) {
+ JS_ClearPendingException(aCx);
+ continue;
+ }
+
+ // Get a reference to the deserialized data for this process.
+ PersistedHistogramArray& deserializedProcessData =
+ histogramsToUpdate[static_cast<uint32_t>(processID)];
+
+ JS::Rooted<JS::PropertyKey> histogram(aCx);
+ for (auto& histogramVal : histograms) {
+ histogram = histogramVal;
+
+ int64_t sum = 0;
+ nsTArray<base::Histogram::Count> deserializedCounts;
+ nsCString histogramName;
+ if (NS_FAILED(internal_ParseHistogramData(aCx, histogram, processDataObj,
+ histogramName,
+ deserializedCounts, sum))) {
+ continue;
+ }
+
+ // Finally append the deserialized data to the storage.
+ if (!deserializedProcessData.emplaceBack(std::make_tuple(
+ std::move(histogramName), std::move(deserializedCounts), sum))) {
+ return NS_ERROR_OUT_OF_MEMORY;
+ }
+ }
+ }
+
+ // Update the histogram storage.
+ {
+ StaticMutexAutoLock locker(gTelemetryHistogramMutex);
+
+ for (uint32_t process = 0; process < histogramsToUpdate.length();
+ ++process) {
+ PersistedHistogramArray& processArray = histogramsToUpdate[process];
+
+ for (auto& histogramData : processArray) {
+ // Attempt to get the corresponding ID for the deserialized histogram
+ // name.
+ HistogramID id;
+ if (NS_FAILED(internal_GetHistogramIdByName(
+ locker, std::get<0>(histogramData), &id))) {
+ continue;
+ }
+
+ ProcessID procID = static_cast<ProcessID>(process);
+ if (!internal_CanRecordHistogram(id, procID)) {
+ // We're not allowed to record this, so don't try to restore it.
+ continue;
+ }
+
+ // Get the Histogram instance: this will instantiate it if it doesn't
+ // exist.
+ Histogram* w = internal_GetHistogramById(locker, id, procID);
+ MOZ_ASSERT(w);
+
+ if (!w || w->IsExpired()) {
+ continue;
+ }
+
+ base::Histogram* h = nullptr;
+ constexpr auto store = "main"_ns;
+ if (!w->GetHistogram(store, &h)) {
+ continue;
+ }
+ MOZ_ASSERT(h);
+
+ if (!h) {
+ // Don't restore expired histograms.
+ continue;
+ }
+
+ // Make sure that histogram counts have matching sizes. If not,
+ // |AddSampleSet| will fail and crash.
+ size_t numCounts = std::get<1>(histogramData).Length();
+ if (h->bucket_count() != numCounts) {
+ MOZ_ASSERT(false,
+ "The number of restored buckets does not match with the "
+ "on in the definition");
+ continue;
+ }
+
+ // Update the data for the histogram.
+ h->AddSampleSet(base::PersistedSampleSet(
+ std::move(std::get<1>(histogramData)), std::get<2>(histogramData)));
+ }
+ }
+ }
+
+ return NS_OK;
+}
+
+nsresult TelemetryHistogram::DeserializeKeyedHistograms(
+ JSContext* aCx, JS::Handle<JS::Value> aData) {
+ MOZ_ASSERT(XRE_IsParentProcess(),
+ "Only load keyed histograms in the parent process");
+ if (!XRE_IsParentProcess()) {
+ return NS_ERROR_FAILURE;
+ }
+
+ // Telemetry is disabled. This should never happen, but let's leave this check
+ // for consistency with other histogram updates routines.
+ if (!internal_CanRecordBase()) {
+ return NS_OK;
+ }
+
+ typedef std::tuple<nsCString, nsCString, nsTArray<base::Histogram::Count>,
+ int64_t>
+ PersistedKeyedHistogramTuple;
+ typedef mozilla::Vector<PersistedKeyedHistogramTuple>
+ PersistedKeyedHistogramArray;
+ typedef mozilla::Vector<PersistedKeyedHistogramArray>
+ PersistedKeyedHistogramStorage;
+
+ // Before updating the histograms, we need to get the data out of the JS
+ // wrappers. We can't hold the histogram mutex while handling JS stuff.
+ // Build a <histogram name, value> map.
+ JS::Rooted<JSObject*> histogramDataObj(aCx, &aData.toObject());
+ JS::Rooted<JS::IdVector> processes(aCx, JS::IdVector(aCx));
+ if (!JS_Enumerate(aCx, histogramDataObj, &processes)) {
+ // We can't even enumerate the processes in the loaded data, so
+ // there is nothing we could recover from the persistence file. Bail out.
+ JS_ClearPendingException(aCx);
+ return NS_ERROR_FAILURE;
+ }
+
+ // Make sure we have enough storage for all the processes.
+ PersistedKeyedHistogramStorage histogramsToUpdate;
+ if (!histogramsToUpdate.resize(static_cast<uint32_t>(ProcessID::Count))) {
+ return NS_ERROR_OUT_OF_MEMORY;
+ }
+
+ // The following block of code attempts to extract as much data as possible
+ // from the serialized JSON, even in case of light data corruptions: if, for
+ // example, the data for a single process is corrupted or is in an unexpected
+ // form, we press on and attempt to load the data for the other processes.
+ JS::Rooted<JS::PropertyKey> process(aCx);
+ for (auto& processVal : processes) {
+ // This is required as JS API calls require an Handle<jsid> and not a
+ // plain jsid.
+ process = processVal;
+ // Get the process name.
+ nsAutoJSString processNameJS;
+ if (!processNameJS.init(aCx, process)) {
+ JS_ClearPendingException(aCx);
+ continue;
+ }
+
+ // Make sure it's valid. Note that this is safe to call outside
+ // of a locked section.
+ NS_ConvertUTF16toUTF8 processName(processNameJS);
+ ProcessID processID = GetIDForProcessName(processName.get());
+ if (processID == ProcessID::Count) {
+ NS_WARNING(
+ nsPrintfCString("Failed to get process ID for %s", processName.get())
+ .get());
+ continue;
+ }
+
+ // And its probes.
+ JS::Rooted<JS::Value> processData(aCx);
+ if (!JS_GetPropertyById(aCx, histogramDataObj, process, &processData)) {
+ JS_ClearPendingException(aCx);
+ continue;
+ }
+
+ if (!processData.isObject()) {
+ // |processData| should be an object containing histograms. If this is
+ // not the case, silently skip and try to load the data for the other
+ // processes.
+ continue;
+ }
+
+ // Iterate through each keyed histogram.
+ JS::Rooted<JSObject*> processDataObj(aCx, &processData.toObject());
+ JS::Rooted<JS::IdVector> histograms(aCx, JS::IdVector(aCx));
+ if (!JS_Enumerate(aCx, processDataObj, &histograms)) {
+ JS_ClearPendingException(aCx);
+ continue;
+ }
+
+ // Get a reference to the deserialized data for this process.
+ PersistedKeyedHistogramArray& deserializedProcessData =
+ histogramsToUpdate[static_cast<uint32_t>(processID)];
+
+ JS::Rooted<JS::PropertyKey> histogram(aCx);
+ for (auto& histogramVal : histograms) {
+ histogram = histogramVal;
+ // Get the histogram name.
+ nsAutoJSString histogramName;
+ if (!histogramName.init(aCx, histogram)) {
+ JS_ClearPendingException(aCx);
+ continue;
+ }
+
+ // Get the data for this histogram.
+ JS::Rooted<JS::Value> histogramData(aCx);
+ if (!JS_GetPropertyById(aCx, processDataObj, histogram, &histogramData)) {
+ JS_ClearPendingException(aCx);
+ continue;
+ }
+
+ // Iterate through each key in the histogram.
+ JS::Rooted<JSObject*> keysDataObj(aCx, &histogramData.toObject());
+ JS::Rooted<JS::IdVector> keys(aCx, JS::IdVector(aCx));
+ if (!JS_Enumerate(aCx, keysDataObj, &keys)) {
+ JS_ClearPendingException(aCx);
+ continue;
+ }
+
+ JS::Rooted<JS::PropertyKey> key(aCx);
+ for (auto& keyVal : keys) {
+ key = keyVal;
+
+ int64_t sum = 0;
+ nsTArray<base::Histogram::Count> deserializedCounts;
+ nsCString keyName;
+ if (NS_FAILED(internal_ParseHistogramData(
+ aCx, key, keysDataObj, keyName, deserializedCounts, sum))) {
+ continue;
+ }
+
+ // Finally append the deserialized data to the storage.
+ if (!deserializedProcessData.emplaceBack(std::make_tuple(
+ nsCString(NS_ConvertUTF16toUTF8(histogramName)),
+ std::move(keyName), std::move(deserializedCounts), sum))) {
+ return NS_ERROR_OUT_OF_MEMORY;
+ }
+ }
+ }
+ }
+
+ // Update the keyed histogram storage.
+ {
+ StaticMutexAutoLock locker(gTelemetryHistogramMutex);
+
+ for (uint32_t process = 0; process < histogramsToUpdate.length();
+ ++process) {
+ PersistedKeyedHistogramArray& processArray = histogramsToUpdate[process];
+
+ for (auto& histogramData : processArray) {
+ // Attempt to get the corresponding ID for the deserialized histogram
+ // name.
+ HistogramID id;
+ if (NS_FAILED(internal_GetHistogramIdByName(
+ locker, std::get<0>(histogramData), &id))) {
+ continue;
+ }
+
+ ProcessID procID = static_cast<ProcessID>(process);
+ if (!internal_CanRecordHistogram(id, procID)) {
+ // We're not allowed to record this, so don't try to restore it.
+ continue;
+ }
+
+ KeyedHistogram* keyed = internal_GetKeyedHistogramById(id, procID);
+ MOZ_ASSERT(keyed);
+
+ if (!keyed || keyed->IsExpired()) {
+ // Don't restore if we don't have a destination storage or the
+ // histogram is expired.
+ continue;
+ }
+
+ // Get data for the key we're looking for.
+ base::Histogram* h = nullptr;
+ if (NS_FAILED(keyed->GetHistogram("main"_ns, std::get<1>(histogramData),
+ &h))) {
+ continue;
+ }
+ MOZ_ASSERT(h);
+
+ if (!h) {
+ // Don't restore if we don't have a destination storage.
+ continue;
+ }
+
+ // Make sure that histogram counts have matching sizes. If not,
+ // |AddSampleSet| will fail and crash.
+ size_t numCounts = std::get<2>(histogramData).Length();
+ if (h->bucket_count() != numCounts) {
+ MOZ_ASSERT(false,
+ "The number of restored buckets does not match with the "
+ "on in the definition");
+ continue;
+ }
+
+ // Update the data for the histogram.
+ h->AddSampleSet(base::PersistedSampleSet(
+ std::move(std::get<2>(histogramData)), std::get<3>(histogramData)));
+ }
+ }
+ }
+
+ return NS_OK;
+}
diff --git a/toolkit/components/telemetry/core/TelemetryHistogram.h b/toolkit/components/telemetry/core/TelemetryHistogram.h
new file mode 100644
index 0000000000..9f415f3637
--- /dev/null
+++ b/toolkit/components/telemetry/core/TelemetryHistogram.h
@@ -0,0 +1,124 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2; -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef TelemetryHistogram_h__
+#define TelemetryHistogram_h__
+
+#include "mozilla/TelemetryComms.h"
+#include "mozilla/TelemetryHistogramEnums.h"
+#include "mozilla/TelemetryProcessEnums.h"
+#include "nsXULAppAPI.h"
+#include "TelemetryCommon.h"
+
+namespace mozilla {
+// This is only used for the GeckoView persistence.
+class JSONWriter;
+} // namespace mozilla
+
+// This module is internal to Telemetry. It encapsulates Telemetry's
+// histogram accumulation and storage logic. It should only be used by
+// Telemetry.cpp. These functions should not be used anywhere else.
+// For the public interface to Telemetry functionality, see Telemetry.h.
+
+namespace TelemetryHistogram {
+
+void InitializeGlobalState(bool canRecordBase, bool canRecordExtended);
+void DeInitializeGlobalState();
+#ifdef DEBUG
+bool GlobalStateHasBeenInitialized();
+#endif
+
+bool CanRecordBase();
+void SetCanRecordBase(bool b);
+bool CanRecordExtended();
+void SetCanRecordExtended(bool b);
+
+void InitHistogramRecordingEnabled();
+void SetHistogramRecordingEnabled(mozilla::Telemetry::HistogramID aID,
+ bool aEnabled);
+
+nsresult SetHistogramRecordingEnabled(const nsACString& id, bool aEnabled);
+
+void Accumulate(mozilla::Telemetry::HistogramID aHistogram, uint32_t aSample);
+void Accumulate(mozilla::Telemetry::HistogramID aHistogram,
+ const nsTArray<uint32_t>& aSamples);
+void Accumulate(mozilla::Telemetry::HistogramID aID, const nsCString& aKey,
+ uint32_t aSample);
+void Accumulate(mozilla::Telemetry::HistogramID aID, const nsCString& aKey,
+ const nsTArray<uint32_t>& aSamples);
+/*
+ * Accumulate a sample into the named histogram.
+ *
+ * Returns NS_OK on success.
+ * Returns NS_ERROR_NOT_AVAILABLE if recording Telemetry is disabled.
+ * Returns NS_ERROR_FAILURE on other errors.
+ */
+nsresult Accumulate(const char* name, uint32_t sample);
+
+/*
+ * Accumulate a sample into the named keyed histogram by key.
+ *
+ * Returns NS_OK on success.
+ * Returns NS_ERROR_NOT_AVAILABLE if recording Telemetry is disabled.
+ * Returns NS_ERROR_FAILURE on other errors.
+ */
+nsresult Accumulate(const char* name, const nsCString& key, uint32_t sample);
+
+void AccumulateCategorical(mozilla::Telemetry::HistogramID aId,
+ const nsCString& aLabel);
+void AccumulateCategorical(mozilla::Telemetry::HistogramID aId,
+ const nsTArray<nsCString>& aLabels);
+
+void AccumulateChild(
+ mozilla::Telemetry::ProcessID aProcessType,
+ const nsTArray<mozilla::Telemetry::HistogramAccumulation>& aAccumulations);
+void AccumulateChildKeyed(
+ mozilla::Telemetry::ProcessID aProcessType,
+ const nsTArray<mozilla::Telemetry::KeyedHistogramAccumulation>&
+ aAccumulations);
+
+/**
+ * Append the list of registered stores to the given set.
+ */
+nsresult GetAllStores(mozilla::Telemetry::Common::StringHashSet& set);
+
+nsresult GetCategoricalHistogramLabels(JSContext* aCx,
+ JS::MutableHandle<JS::Value> aResult);
+
+nsresult GetHistogramById(const nsACString& name, JSContext* cx,
+ JS::MutableHandle<JS::Value> ret);
+
+nsresult GetKeyedHistogramById(const nsACString& name, JSContext* cx,
+ JS::MutableHandle<JS::Value> ret);
+
+const char* GetHistogramName(mozilla::Telemetry::HistogramID id);
+
+nsresult CreateHistogramSnapshots(JSContext* aCx,
+ JS::MutableHandle<JS::Value> aResult,
+ const nsACString& aStore,
+ unsigned int aDataset, bool aClearSubsession,
+ bool aFilterTest = false);
+
+nsresult GetKeyedHistogramSnapshots(JSContext* aCx,
+ JS::MutableHandle<JS::Value> aResult,
+ const nsACString& aStore,
+ unsigned int aDataset,
+ bool aClearSubsession,
+ bool aFilterTest = false);
+
+size_t GetHistogramSizesOfIncludingThis(mozilla::MallocSizeOf aMallocSizeOf);
+
+// These functions are only meant to be used for GeckoView persistence.
+// They are responsible for updating in-memory probes with the data persisted
+// on the disk and vice-versa.
+nsresult SerializeHistograms(mozilla::JSONWriter& aWriter);
+nsresult SerializeKeyedHistograms(mozilla::JSONWriter& aWriter);
+nsresult DeserializeHistograms(JSContext* aCx, JS::Handle<JS::Value> aData);
+nsresult DeserializeKeyedHistograms(JSContext* aCx,
+ JS::Handle<JS::Value> aData);
+
+} // namespace TelemetryHistogram
+
+#endif // TelemetryHistogram_h__
diff --git a/toolkit/components/telemetry/core/TelemetryScalar.cpp b/toolkit/components/telemetry/core/TelemetryScalar.cpp
new file mode 100644
index 0000000000..8a121e8f3f
--- /dev/null
+++ b/toolkit/components/telemetry/core/TelemetryScalar.cpp
@@ -0,0 +1,4190 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "TelemetryScalar.h"
+
+#include "geckoview/streaming/GeckoViewStreamingTelemetry.h"
+#include "ipc/TelemetryIPCAccumulator.h"
+#include "js/Array.h" // JS::GetArrayLength, JS::IsArrayObject
+#include "js/PropertyAndElement.h" // JS_DefineProperty, JS_DefineUCProperty, JS_Enumerate, JS_GetElement, JS_GetProperty, JS_GetPropertyById, JS_HasProperty
+#include "mozilla/dom/ContentParent.h"
+#include "mozilla/JSONWriter.h"
+#include "mozilla/Preferences.h"
+#include "mozilla/StaticMutex.h"
+#include "mozilla/StaticPtr.h"
+#include "mozilla/TelemetryComms.h"
+#include "mozilla/Unused.h"
+#include "nsBaseHashtable.h"
+#include "nsClassHashtable.h"
+#include "nsContentUtils.h"
+#include "nsHashKeys.h"
+#include "nsITelemetry.h"
+#include "nsIVariant.h"
+#include "nsIXPConnect.h"
+#include "nsJSUtils.h"
+#include "nsPrintfCString.h"
+#include "nsVariant.h"
+#include "TelemetryScalarData.h"
+
+using mozilla::MakeUnique;
+using mozilla::Nothing;
+using mozilla::Preferences;
+using mozilla::Some;
+using mozilla::StaticAutoPtr;
+using mozilla::StaticMutex;
+using mozilla::StaticMutexAutoLock;
+using mozilla::UniquePtr;
+using mozilla::Telemetry::DynamicScalarDefinition;
+using mozilla::Telemetry::KeyedScalarAction;
+using mozilla::Telemetry::ProcessID;
+using mozilla::Telemetry::ScalarAction;
+using mozilla::Telemetry::ScalarActionType;
+using mozilla::Telemetry::ScalarID;
+using mozilla::Telemetry::ScalarVariant;
+using mozilla::Telemetry::Common::AutoHashtable;
+using mozilla::Telemetry::Common::CanRecordDataset;
+using mozilla::Telemetry::Common::CanRecordProduct;
+using mozilla::Telemetry::Common::GetCurrentProduct;
+using mozilla::Telemetry::Common::GetIDForProcessName;
+using mozilla::Telemetry::Common::GetNameForProcessID;
+using mozilla::Telemetry::Common::IsExpiredVersion;
+using mozilla::Telemetry::Common::IsInDataset;
+using mozilla::Telemetry::Common::IsValidIdentifierString;
+using mozilla::Telemetry::Common::LogToBrowserConsole;
+using mozilla::Telemetry::Common::RecordedProcessType;
+using mozilla::Telemetry::Common::StringHashSet;
+using mozilla::Telemetry::Common::SupportedProduct;
+
+namespace TelemetryIPCAccumulator = mozilla::TelemetryIPCAccumulator;
+
+////////////////////////////////////////////////////////////////////////
+////////////////////////////////////////////////////////////////////////
+//
+// Naming: there are two kinds of functions in this file:
+//
+// * Functions named internal_*: these can only be reached via an
+// interface function (TelemetryScalar::*). If they access shared
+// state, they require the interface function to have acquired
+// |gTelemetryScalarMutex| to ensure thread safety.
+//
+// * Functions named TelemetryScalar::*. This is the external interface.
+// Entries and exits to these functions are serialised using
+// |gTelemetryScalarsMutex|.
+//
+// Avoiding races and deadlocks:
+//
+// All functions in the external interface (TelemetryScalar::*) are
+// serialised using the mutex |gTelemetryScalarsMutex|. This means
+// that the external interface is thread-safe. But it also brings
+// a danger of deadlock if any function in the external interface can
+// get back to that interface. That is, we will deadlock on any call
+// chain like this
+//
+// TelemetryScalar::* -> .. any functions .. -> TelemetryScalar::*
+//
+// To reduce the danger of that happening, observe the following rules:
+//
+// * No function in TelemetryScalar::* may directly call, nor take the
+// address of, any other function in TelemetryScalar::*.
+//
+// * No internal function internal_* may call, nor take the address
+// of, any function in TelemetryScalar::*.
+
+////////////////////////////////////////////////////////////////////////
+////////////////////////////////////////////////////////////////////////
+//
+// PRIVATE TYPES
+
+namespace {
+
+const uint32_t kMaximumNumberOfKeys = 100;
+const uint32_t kMaxEventSummaryKeys = 500;
+const uint32_t kMaximumKeyStringLength = 72;
+const uint32_t kMaximumStringValueLength = 50;
+// The category and scalar name maximum lengths are used by the dynamic
+// scalar registration function and must match the constants used by
+// the 'parse_scalars.py' script for static scalars.
+const uint32_t kMaximumCategoryNameLength = 40;
+const uint32_t kMaximumScalarNameLength = 40;
+const uint32_t kScalarCount =
+ static_cast<uint32_t>(mozilla::Telemetry::ScalarID::ScalarCount);
+
+// To stop growing unbounded in memory while waiting for scalar deserialization
+// to finish, we immediately apply pending operations if the array reaches
+// a certain high water mark of elements.
+const size_t kScalarActionsArrayHighWaterMark = 10000;
+
+const char* TEST_SCALAR_PREFIX = "telemetry.test.";
+
+// The max offset supported by gScalarStoresTable for static scalars' stores.
+// Also the sentinel value (with store_count == 0) for just the sole "main"
+// store.
+const uint32_t kMaxStaticStoreOffset = UINT16_MAX;
+
+enum class ScalarResult : uint8_t {
+ // Nothing went wrong.
+ Ok,
+ // General Scalar Errors
+ NotInitialized,
+ CannotUnpackVariant,
+ CannotRecordInProcess,
+ CannotRecordDataset,
+ KeyedTypeMismatch,
+ UnknownScalar,
+ OperationNotSupported,
+ InvalidType,
+ InvalidValue,
+ // Keyed Scalar Errors
+ KeyIsEmpty,
+ KeyTooLong,
+ TooManyKeys,
+ KeyNotAllowed,
+ // String Scalar Errors
+ StringTooLong,
+ // Unsigned Scalar Errors
+ UnsignedNegativeValue,
+ UnsignedTruncatedValue,
+};
+
+// A common identifier for both built-in and dynamic scalars.
+struct ScalarKey {
+ uint32_t id;
+ bool dynamic;
+};
+
+// Dynamic scalar store names.
+StaticAutoPtr<nsTArray<RefPtr<nsAtom>>> gDynamicStoreNames;
+
+/**
+ * Scalar information for dynamic definitions.
+ */
+struct DynamicScalarInfo : BaseScalarInfo {
+ nsCString mDynamicName;
+ bool mDynamicExpiration;
+ uint32_t store_count;
+ uint32_t store_offset;
+
+ DynamicScalarInfo(uint32_t aKind, bool aRecordOnRelease, bool aExpired,
+ const nsACString& aName, bool aKeyed, bool aBuiltin,
+ const nsTArray<nsCString>& aStores)
+ : BaseScalarInfo(aKind,
+ aRecordOnRelease
+ ? nsITelemetry::DATASET_ALL_CHANNELS
+ : nsITelemetry::DATASET_PRERELEASE_CHANNELS,
+ RecordedProcessType::All, aKeyed, 0, 0,
+ GetCurrentProduct(), aBuiltin),
+ mDynamicName(aName),
+ mDynamicExpiration(aExpired) {
+ store_count = aStores.Length();
+ if (store_count == 0) {
+ store_count = 1;
+ store_offset = kMaxStaticStoreOffset;
+ } else {
+ store_offset = kMaxStaticStoreOffset + 1 + gDynamicStoreNames->Length();
+ for (const auto& storeName : aStores) {
+ gDynamicStoreNames->AppendElement(NS_Atomize(storeName));
+ }
+ MOZ_ASSERT(
+ gDynamicStoreNames->Length() < UINT32_MAX - kMaxStaticStoreOffset - 1,
+ "Too many dynamic scalar store names. Overflow.");
+ }
+ };
+
+ // The following functions will read the stored text
+ // instead of looking it up in the statically generated
+ // tables.
+ const char* name() const override;
+ const char* expiration() const override;
+
+ uint32_t storeCount() const override;
+ uint32_t storeOffset() const override;
+};
+
+const char* DynamicScalarInfo::name() const { return mDynamicName.get(); }
+
+const char* DynamicScalarInfo::expiration() const {
+ // Dynamic scalars can either be expired or not (boolean flag).
+ // Return an appropriate version string to leverage the scalar expiration
+ // logic.
+ return mDynamicExpiration ? "1.0" : "never";
+}
+
+uint32_t DynamicScalarInfo::storeOffset() const { return store_offset; }
+uint32_t DynamicScalarInfo::storeCount() const { return store_count; }
+
+typedef nsBaseHashtableET<nsDepCharHashKey, ScalarKey> CharPtrEntryType;
+typedef AutoHashtable<CharPtrEntryType> ScalarMapType;
+
+// Dynamic scalar definitions.
+StaticAutoPtr<nsTArray<DynamicScalarInfo>> gDynamicScalarInfo;
+
+const BaseScalarInfo& internal_GetScalarInfo(const StaticMutexAutoLock& lock,
+ const ScalarKey& aId) {
+ if (!aId.dynamic) {
+ return gScalars[aId.id];
+ }
+
+ return (*gDynamicScalarInfo)[aId.id];
+}
+
+bool IsValidEnumId(mozilla::Telemetry::ScalarID aID) {
+ return aID < mozilla::Telemetry::ScalarID::ScalarCount;
+}
+
+bool internal_IsValidId(const StaticMutexAutoLock& lock, const ScalarKey& aId) {
+ // Please note that this function needs to be called with the scalar
+ // mutex being acquired: other functions might be messing with
+ // |gDynamicScalarInfo|.
+ return aId.dynamic
+ ? (aId.id < gDynamicScalarInfo->Length())
+ : IsValidEnumId(static_cast<mozilla::Telemetry::ScalarID>(aId.id));
+}
+
+/**
+ * Convert a nsIVariant to a mozilla::Variant, which is used for
+ * accumulating child process scalars.
+ */
+ScalarResult GetVariantFromIVariant(nsIVariant* aInput, uint32_t aScalarKind,
+ mozilla::Maybe<ScalarVariant>& aOutput) {
+ switch (aScalarKind) {
+ case nsITelemetry::SCALAR_TYPE_COUNT: {
+ uint32_t val = 0;
+ nsresult rv = aInput->GetAsUint32(&val);
+ if (NS_FAILED(rv)) {
+ return ScalarResult::CannotUnpackVariant;
+ }
+ aOutput = mozilla::Some(mozilla::AsVariant(val));
+ break;
+ }
+ case nsITelemetry::SCALAR_TYPE_STRING: {
+ nsString val;
+ nsresult rv = aInput->GetAsAString(val);
+ if (NS_FAILED(rv)) {
+ return ScalarResult::CannotUnpackVariant;
+ }
+ aOutput = mozilla::Some(mozilla::AsVariant(val));
+ break;
+ }
+ case nsITelemetry::SCALAR_TYPE_BOOLEAN: {
+ bool val = false;
+ nsresult rv = aInput->GetAsBool(&val);
+ if (NS_FAILED(rv)) {
+ return ScalarResult::CannotUnpackVariant;
+ }
+ aOutput = mozilla::Some(mozilla::AsVariant(val));
+ break;
+ }
+ default:
+ MOZ_ASSERT(false, "Unknown scalar kind.");
+ return ScalarResult::UnknownScalar;
+ }
+ return ScalarResult::Ok;
+}
+
+/**
+ * Write a nsIVariant with a JSONWriter, used for GeckoView persistence.
+ */
+nsresult WriteVariantToJSONWriter(
+ uint32_t aScalarType, nsIVariant* aInputValue,
+ const mozilla::Span<const char>& aPropertyName,
+ mozilla::JSONWriter& aWriter) {
+ MOZ_ASSERT(aInputValue);
+
+ switch (aScalarType) {
+ case nsITelemetry::SCALAR_TYPE_COUNT: {
+ uint32_t val = 0;
+ nsresult rv = aInputValue->GetAsUint32(&val);
+ NS_ENSURE_SUCCESS(rv, rv);
+ aWriter.IntProperty(aPropertyName, val);
+ break;
+ }
+ case nsITelemetry::SCALAR_TYPE_STRING: {
+ nsCString val;
+ nsresult rv = aInputValue->GetAsACString(val);
+ NS_ENSURE_SUCCESS(rv, rv);
+ aWriter.StringProperty(aPropertyName, val);
+ break;
+ }
+ case nsITelemetry::SCALAR_TYPE_BOOLEAN: {
+ bool val = false;
+ nsresult rv = aInputValue->GetAsBool(&val);
+ NS_ENSURE_SUCCESS(rv, rv);
+ aWriter.BoolProperty(aPropertyName, val);
+ break;
+ }
+ default:
+ MOZ_ASSERT(false, "Unknown scalar kind.");
+ return NS_ERROR_FAILURE;
+ }
+
+ return NS_OK;
+}
+
+// Implements the methods for ScalarInfo.
+const char* ScalarInfo::name() const {
+ return &gScalarsStringTable[this->name_offset];
+}
+
+const char* ScalarInfo::expiration() const {
+ return &gScalarsStringTable[this->expiration_offset];
+}
+
+/**
+ * The base scalar object, that serves as a common ancestor for storage
+ * purposes.
+ */
+class ScalarBase {
+ public:
+ explicit ScalarBase(const BaseScalarInfo& aInfo)
+ : mStoreCount(aInfo.storeCount()),
+ mStoreOffset(aInfo.storeOffset()),
+ mStoreHasValue(mStoreCount),
+ mName(aInfo.name()) {
+ mStoreHasValue.SetLength(mStoreCount);
+ for (auto& val : mStoreHasValue) {
+ val = false;
+ }
+ };
+ virtual ~ScalarBase() = default;
+
+ // Set, Add and SetMaximum functions as described in the Telemetry IDL.
+ virtual ScalarResult SetValue(nsIVariant* aValue) = 0;
+ virtual ScalarResult AddValue(nsIVariant* aValue) {
+ return ScalarResult::OperationNotSupported;
+ }
+ virtual ScalarResult SetMaximum(nsIVariant* aValue) {
+ return ScalarResult::OperationNotSupported;
+ }
+
+ // Convenience methods used by the C++ API.
+ virtual void SetValue(uint32_t aValue) {
+ mozilla::Unused << HandleUnsupported();
+ }
+ virtual ScalarResult SetValue(const nsAString& aValue) {
+ return HandleUnsupported();
+ }
+ virtual void SetValue(bool aValue) { mozilla::Unused << HandleUnsupported(); }
+ virtual void AddValue(uint32_t aValue) {
+ mozilla::Unused << HandleUnsupported();
+ }
+ virtual void SetMaximum(uint32_t aValue) {
+ mozilla::Unused << HandleUnsupported();
+ }
+
+ // GetValue is used to get the value of the scalar when persisting it to JS.
+ virtual nsresult GetValue(const nsACString& aStoreName, bool aClearStore,
+ nsCOMPtr<nsIVariant>& aResult) = 0;
+
+ // To measure the memory stats.
+ size_t SizeOfExcludingThis(mozilla::MallocSizeOf aMallocSizeOf) const;
+ virtual size_t SizeOfIncludingThis(
+ mozilla::MallocSizeOf aMallocSizeOf) const = 0;
+
+ protected:
+ bool HasValueInStore(size_t aStoreIndex) const;
+ void ClearValueInStore(size_t aStoreIndex);
+ void SetValueInStores();
+ nsresult StoreIndex(const nsACString& aStoreName, size_t* aStoreIndex) const;
+
+ private:
+ ScalarResult HandleUnsupported() const;
+
+ const uint32_t mStoreCount;
+ const uint32_t mStoreOffset;
+ nsTArray<bool> mStoreHasValue;
+
+ protected:
+ const nsCString mName;
+};
+
+ScalarResult ScalarBase::HandleUnsupported() const {
+ MOZ_ASSERT(false, "This operation is not support for this scalar type.");
+ return ScalarResult::OperationNotSupported;
+}
+
+bool ScalarBase::HasValueInStore(size_t aStoreIndex) const {
+ MOZ_ASSERT(aStoreIndex < mStoreHasValue.Length(),
+ "Invalid scalar store index.");
+ return mStoreHasValue[aStoreIndex];
+}
+
+void ScalarBase::ClearValueInStore(size_t aStoreIndex) {
+ MOZ_ASSERT(aStoreIndex < mStoreHasValue.Length(),
+ "Invalid scalar store index to clear.");
+ mStoreHasValue[aStoreIndex] = false;
+}
+
+void ScalarBase::SetValueInStores() {
+ for (auto& val : mStoreHasValue) {
+ val = true;
+ }
+}
+
+nsresult ScalarBase::StoreIndex(const nsACString& aStoreName,
+ size_t* aStoreIndex) const {
+ if (mStoreCount == 1 && mStoreOffset == kMaxStaticStoreOffset) {
+ // This Scalar is only in the "main" store.
+ if (aStoreName.EqualsLiteral("main")) {
+ *aStoreIndex = 0;
+ return NS_OK;
+ }
+ return NS_ERROR_NO_CONTENT;
+ }
+
+ // Multiple stores. Linear scan to find one that matches aStoreName.
+ // Dynamic Scalars start at kMaxStaticStoreOffset + 1
+ if (mStoreOffset > kMaxStaticStoreOffset) {
+ auto dynamicOffset = mStoreOffset - kMaxStaticStoreOffset - 1;
+ for (uint32_t i = 0; i < mStoreCount; ++i) {
+ auto scalarStore = (*gDynamicStoreNames)[dynamicOffset + i];
+ if (nsAtomCString(scalarStore).Equals(aStoreName)) {
+ *aStoreIndex = i;
+ return NS_OK;
+ }
+ }
+ return NS_ERROR_NO_CONTENT;
+ }
+
+ // Static Scalars are similar.
+ for (uint32_t i = 0; i < mStoreCount; ++i) {
+ uint32_t stringIndex = gScalarStoresTable[mStoreOffset + i];
+ if (aStoreName.EqualsASCII(&gScalarsStringTable[stringIndex])) {
+ *aStoreIndex = i;
+ return NS_OK;
+ }
+ }
+ return NS_ERROR_NO_CONTENT;
+}
+
+size_t ScalarBase::SizeOfExcludingThis(
+ mozilla::MallocSizeOf aMallocSizeOf) const {
+ return mStoreHasValue.ShallowSizeOfExcludingThis(aMallocSizeOf);
+}
+
+/**
+ * The implementation for the unsigned int scalar type.
+ */
+class ScalarUnsigned : public ScalarBase {
+ public:
+ using ScalarBase::SetValue;
+
+ explicit ScalarUnsigned(const BaseScalarInfo& aInfo)
+ : ScalarBase(aInfo), mStorage(aInfo.storeCount()) {
+ mStorage.SetLength(aInfo.storeCount());
+ for (auto& val : mStorage) {
+ val = 0;
+ }
+ };
+
+ ~ScalarUnsigned() override = default;
+
+ ScalarResult SetValue(nsIVariant* aValue) final;
+ void SetValue(uint32_t aValue) final;
+ ScalarResult AddValue(nsIVariant* aValue) final;
+ void AddValue(uint32_t aValue) final;
+ ScalarResult SetMaximum(nsIVariant* aValue) final;
+ void SetMaximum(uint32_t aValue) final;
+ nsresult GetValue(const nsACString& aStoreName, bool aClearStore,
+ nsCOMPtr<nsIVariant>& aResult) final;
+ size_t SizeOfIncludingThis(mozilla::MallocSizeOf aMallocSizeOf) const final;
+
+ private:
+ nsTArray<uint32_t> mStorage;
+
+ ScalarResult CheckInput(nsIVariant* aValue);
+
+ // Prevent copying.
+ ScalarUnsigned(const ScalarUnsigned& aOther) = delete;
+ void operator=(const ScalarUnsigned& aOther) = delete;
+};
+
+ScalarResult ScalarUnsigned::SetValue(nsIVariant* aValue) {
+ ScalarResult sr = CheckInput(aValue);
+ if (sr == ScalarResult::UnsignedNegativeValue) {
+ return sr;
+ }
+
+ uint32_t value = 0;
+ if (NS_FAILED(aValue->GetAsUint32(&value))) {
+ return ScalarResult::InvalidValue;
+ }
+
+ SetValue(value);
+ return sr;
+}
+
+void ScalarUnsigned::SetValue(uint32_t aValue) {
+ if (GetCurrentProduct() == SupportedProduct::GeckoviewStreaming) {
+ GeckoViewStreamingTelemetry::UintScalarSet(mName, aValue);
+ return;
+ }
+ for (auto& val : mStorage) {
+ val = aValue;
+ }
+ SetValueInStores();
+}
+
+ScalarResult ScalarUnsigned::AddValue(nsIVariant* aValue) {
+ ScalarResult sr = CheckInput(aValue);
+ if (sr == ScalarResult::UnsignedNegativeValue) {
+ return sr;
+ }
+
+ uint32_t newAddend = 0;
+ nsresult rv = aValue->GetAsUint32(&newAddend);
+ if (NS_FAILED(rv)) {
+ return ScalarResult::InvalidValue;
+ }
+
+ AddValue(newAddend);
+ return sr;
+}
+
+void ScalarUnsigned::AddValue(uint32_t aValue) {
+ MOZ_ASSERT(GetCurrentProduct() != SupportedProduct::GeckoviewStreaming);
+ for (auto& val : mStorage) {
+ val += aValue;
+ }
+ SetValueInStores();
+}
+
+ScalarResult ScalarUnsigned::SetMaximum(nsIVariant* aValue) {
+ MOZ_ASSERT(GetCurrentProduct() != SupportedProduct::GeckoviewStreaming);
+ ScalarResult sr = CheckInput(aValue);
+ if (sr == ScalarResult::UnsignedNegativeValue) {
+ return sr;
+ }
+
+ uint32_t newValue = 0;
+ nsresult rv = aValue->GetAsUint32(&newValue);
+ if (NS_FAILED(rv)) {
+ return ScalarResult::InvalidValue;
+ }
+
+ SetMaximum(newValue);
+ return sr;
+}
+
+void ScalarUnsigned::SetMaximum(uint32_t aValue) {
+ for (auto& val : mStorage) {
+ if (aValue > val) {
+ val = aValue;
+ }
+ }
+ SetValueInStores();
+}
+
+nsresult ScalarUnsigned::GetValue(const nsACString& aStoreName,
+ bool aClearStore,
+ nsCOMPtr<nsIVariant>& aResult) {
+ size_t storeIndex = 0;
+ nsresult rv = StoreIndex(aStoreName, &storeIndex);
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+ if (!HasValueInStore(storeIndex)) {
+ return NS_ERROR_NO_CONTENT;
+ }
+ nsCOMPtr<nsIWritableVariant> outVar(new nsVariant());
+ rv = outVar->SetAsUint32(mStorage[storeIndex]);
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+ aResult = std::move(outVar);
+ if (aClearStore) {
+ mStorage[storeIndex] = 0;
+ ClearValueInStore(storeIndex);
+ }
+ return NS_OK;
+}
+
+size_t ScalarUnsigned::SizeOfIncludingThis(
+ mozilla::MallocSizeOf aMallocSizeOf) const {
+ size_t n = aMallocSizeOf(this);
+ n += ScalarBase::SizeOfExcludingThis(aMallocSizeOf);
+ n += mStorage.ShallowSizeOfExcludingThis(aMallocSizeOf);
+ return n;
+}
+
+ScalarResult ScalarUnsigned::CheckInput(nsIVariant* aValue) {
+ // If this is a floating point value/double, we will probably get truncated.
+ uint16_t type = aValue->GetDataType();
+ if (type == nsIDataType::VTYPE_FLOAT || type == nsIDataType::VTYPE_DOUBLE) {
+ return ScalarResult::UnsignedTruncatedValue;
+ }
+
+ int32_t signedTest;
+ // If we're able to cast the number to an int, check its sign.
+ // Warn the user if he's trying to set the unsigned scalar to a negative
+ // number.
+ if (NS_SUCCEEDED(aValue->GetAsInt32(&signedTest)) && signedTest < 0) {
+ return ScalarResult::UnsignedNegativeValue;
+ }
+ return ScalarResult::Ok;
+}
+
+/**
+ * The implementation for the string scalar type.
+ */
+class ScalarString : public ScalarBase {
+ public:
+ using ScalarBase::SetValue;
+
+ explicit ScalarString(const BaseScalarInfo& aInfo)
+ : ScalarBase(aInfo), mStorage(aInfo.storeCount()) {
+ mStorage.SetLength(aInfo.storeCount());
+ };
+
+ ~ScalarString() override = default;
+
+ ScalarResult SetValue(nsIVariant* aValue) final;
+ ScalarResult SetValue(const nsAString& aValue) final;
+ nsresult GetValue(const nsACString& aStoreName, bool aClearStore,
+ nsCOMPtr<nsIVariant>& aResult) final;
+ size_t SizeOfIncludingThis(mozilla::MallocSizeOf aMallocSizeOf) const final;
+
+ private:
+ nsTArray<nsString> mStorage;
+
+ // Prevent copying.
+ ScalarString(const ScalarString& aOther) = delete;
+ void operator=(const ScalarString& aOther) = delete;
+};
+
+ScalarResult ScalarString::SetValue(nsIVariant* aValue) {
+ // Check that we got the correct data type.
+ uint16_t type = aValue->GetDataType();
+ if (type != nsIDataType::VTYPE_CHAR && type != nsIDataType::VTYPE_WCHAR &&
+ type != nsIDataType::VTYPE_CHAR_STR &&
+ type != nsIDataType::VTYPE_WCHAR_STR &&
+ type != nsIDataType::VTYPE_STRING_SIZE_IS &&
+ type != nsIDataType::VTYPE_WSTRING_SIZE_IS &&
+ type != nsIDataType::VTYPE_UTF8STRING &&
+ type != nsIDataType::VTYPE_CSTRING &&
+ type != nsIDataType::VTYPE_ASTRING) {
+ return ScalarResult::InvalidType;
+ }
+
+ nsAutoString convertedString;
+ nsresult rv = aValue->GetAsAString(convertedString);
+ if (NS_FAILED(rv)) {
+ return ScalarResult::InvalidValue;
+ }
+ return SetValue(convertedString);
+};
+
+ScalarResult ScalarString::SetValue(const nsAString& aValue) {
+ auto str = Substring(aValue, 0, kMaximumStringValueLength);
+ if (GetCurrentProduct() == SupportedProduct::GeckoviewStreaming) {
+ GeckoViewStreamingTelemetry::StringScalarSet(mName,
+ NS_ConvertUTF16toUTF8(str));
+ return aValue.Length() > kMaximumStringValueLength
+ ? ScalarResult::StringTooLong
+ : ScalarResult::Ok;
+ }
+ for (auto& val : mStorage) {
+ val.Assign(str);
+ }
+ SetValueInStores();
+ if (aValue.Length() > kMaximumStringValueLength) {
+ return ScalarResult::StringTooLong;
+ }
+ return ScalarResult::Ok;
+}
+
+nsresult ScalarString::GetValue(const nsACString& aStoreName, bool aClearStore,
+ nsCOMPtr<nsIVariant>& aResult) {
+ nsCOMPtr<nsIWritableVariant> outVar(new nsVariant());
+ size_t storeIndex = 0;
+ nsresult rv = StoreIndex(aStoreName, &storeIndex);
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+ if (!HasValueInStore(storeIndex)) {
+ return NS_ERROR_NO_CONTENT;
+ }
+ rv = outVar->SetAsAString(mStorage[storeIndex]);
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+ if (aClearStore) {
+ ClearValueInStore(storeIndex);
+ }
+ aResult = std::move(outVar);
+ return NS_OK;
+}
+
+size_t ScalarString::SizeOfIncludingThis(
+ mozilla::MallocSizeOf aMallocSizeOf) const {
+ size_t n = aMallocSizeOf(this);
+ n += ScalarBase::SizeOfExcludingThis(aMallocSizeOf);
+ n += mStorage.ShallowSizeOfExcludingThis(aMallocSizeOf);
+ for (auto& val : mStorage) {
+ n += val.SizeOfExcludingThisIfUnshared(aMallocSizeOf);
+ }
+ return n;
+}
+
+/**
+ * The implementation for the boolean scalar type.
+ */
+class ScalarBoolean : public ScalarBase {
+ public:
+ using ScalarBase::SetValue;
+
+ explicit ScalarBoolean(const BaseScalarInfo& aInfo)
+ : ScalarBase(aInfo), mStorage(aInfo.storeCount()) {
+ mStorage.SetLength(aInfo.storeCount());
+ for (auto& val : mStorage) {
+ val = false;
+ }
+ };
+
+ ~ScalarBoolean() override = default;
+
+ ScalarResult SetValue(nsIVariant* aValue) final;
+ void SetValue(bool aValue) final;
+ nsresult GetValue(const nsACString& aStoreName, bool aClearStore,
+ nsCOMPtr<nsIVariant>& aResult) final;
+ size_t SizeOfIncludingThis(mozilla::MallocSizeOf aMallocSizeOf) const final;
+
+ private:
+ nsTArray<bool> mStorage;
+
+ // Prevent copying.
+ ScalarBoolean(const ScalarBoolean& aOther) = delete;
+ void operator=(const ScalarBoolean& aOther) = delete;
+};
+
+ScalarResult ScalarBoolean::SetValue(nsIVariant* aValue) {
+ // Check that we got the correct data type.
+ uint16_t type = aValue->GetDataType();
+ if (type != nsIDataType::VTYPE_BOOL && type != nsIDataType::VTYPE_INT8 &&
+ type != nsIDataType::VTYPE_INT16 && type != nsIDataType::VTYPE_INT32 &&
+ type != nsIDataType::VTYPE_INT64 && type != nsIDataType::VTYPE_UINT8 &&
+ type != nsIDataType::VTYPE_UINT16 && type != nsIDataType::VTYPE_UINT32 &&
+ type != nsIDataType::VTYPE_UINT64) {
+ return ScalarResult::InvalidType;
+ }
+
+ bool value = false;
+ if (NS_FAILED(aValue->GetAsBool(&value))) {
+ return ScalarResult::InvalidValue;
+ }
+ SetValue(value);
+ return ScalarResult::Ok;
+};
+
+void ScalarBoolean::SetValue(bool aValue) {
+ if (GetCurrentProduct() == SupportedProduct::GeckoviewStreaming) {
+ GeckoViewStreamingTelemetry::BoolScalarSet(mName, aValue);
+ return;
+ }
+ for (auto& val : mStorage) {
+ val = aValue;
+ }
+ SetValueInStores();
+}
+
+nsresult ScalarBoolean::GetValue(const nsACString& aStoreName, bool aClearStore,
+ nsCOMPtr<nsIVariant>& aResult) {
+ nsCOMPtr<nsIWritableVariant> outVar(new nsVariant());
+ size_t storeIndex = 0;
+ nsresult rv = StoreIndex(aStoreName, &storeIndex);
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+ if (!HasValueInStore(storeIndex)) {
+ return NS_ERROR_NO_CONTENT;
+ }
+ if (aClearStore) {
+ ClearValueInStore(storeIndex);
+ }
+ rv = outVar->SetAsBool(mStorage[storeIndex]);
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+ aResult = std::move(outVar);
+ return NS_OK;
+}
+
+size_t ScalarBoolean::SizeOfIncludingThis(
+ mozilla::MallocSizeOf aMallocSizeOf) const {
+ size_t n = aMallocSizeOf(this);
+ n += ScalarBase::SizeOfExcludingThis(aMallocSizeOf);
+ n += mStorage.ShallowSizeOfExcludingThis(aMallocSizeOf);
+ return n;
+}
+
+/**
+ * Allocate a scalar class given the scalar info.
+ *
+ * @param aInfo The informations for the scalar coming from the definition file.
+ * @return nullptr if the scalar type is unknown, otherwise a valid pointer to
+ * the scalar type.
+ */
+ScalarBase* internal_ScalarAllocate(const BaseScalarInfo& aInfo) {
+ ScalarBase* scalar = nullptr;
+ switch (aInfo.kind) {
+ case nsITelemetry::SCALAR_TYPE_COUNT:
+ scalar = new ScalarUnsigned(aInfo);
+ break;
+ case nsITelemetry::SCALAR_TYPE_STRING:
+ scalar = new ScalarString(aInfo);
+ break;
+ case nsITelemetry::SCALAR_TYPE_BOOLEAN:
+ scalar = new ScalarBoolean(aInfo);
+ break;
+ default:
+ MOZ_ASSERT(false, "Invalid scalar type");
+ }
+ return scalar;
+}
+
+/**
+ * The implementation for the keyed scalar type.
+ */
+class KeyedScalar {
+ public:
+ typedef std::pair<nsCString, nsCOMPtr<nsIVariant>> KeyValuePair;
+
+ // We store the name instead of a reference to the BaseScalarInfo because
+ // the BaseScalarInfo can move if it's from a dynamic scalar.
+ explicit KeyedScalar(const BaseScalarInfo& info)
+ : mScalarName(info.name()),
+ mScalarKeyCount(info.key_count),
+ mScalarKeyOffset(info.key_offset),
+ mMaximumNumberOfKeys(kMaximumNumberOfKeys){};
+ ~KeyedScalar() = default;
+
+ // Set, Add and SetMaximum functions as described in the Telemetry IDL.
+ // These methods implicitly instantiate a Scalar[*] for each key.
+ ScalarResult SetValue(const StaticMutexAutoLock& locker,
+ const nsAString& aKey, nsIVariant* aValue);
+ ScalarResult AddValue(const StaticMutexAutoLock& locker,
+ const nsAString& aKey, nsIVariant* aValue);
+ ScalarResult SetMaximum(const StaticMutexAutoLock& locker,
+ const nsAString& aKey, nsIVariant* aValue);
+
+ // Convenience methods used by the C++ API.
+ void SetValue(const StaticMutexAutoLock& locker, const nsAString& aKey,
+ uint32_t aValue);
+ void SetValue(const StaticMutexAutoLock& locker, const nsAString& aKey,
+ bool aValue);
+ void AddValue(const StaticMutexAutoLock& locker, const nsAString& aKey,
+ uint32_t aValue);
+ void SetMaximum(const StaticMutexAutoLock& locker, const nsAString& aKey,
+ uint32_t aValue);
+
+ // GetValue is used to get the key-value pairs stored in the keyed scalar
+ // when persisting it to JS.
+ nsresult GetValue(const nsACString& aStoreName, bool aClearStorage,
+ nsTArray<KeyValuePair>& aValues);
+
+ // To measure the memory stats.
+ size_t SizeOfIncludingThis(mozilla::MallocSizeOf aMallocSizeOf);
+
+ // To permit more keys than normal.
+ void SetMaximumNumberOfKeys(uint32_t aMaximumNumberOfKeys) {
+ mMaximumNumberOfKeys = aMaximumNumberOfKeys;
+ };
+
+ private:
+ typedef nsClassHashtable<nsCStringHashKey, ScalarBase> ScalarKeysMapType;
+
+ const nsCString mScalarName;
+ ScalarKeysMapType mScalarKeys;
+ uint32_t mScalarKeyCount;
+ uint32_t mScalarKeyOffset;
+ uint32_t mMaximumNumberOfKeys;
+
+ ScalarResult GetScalarForKey(const StaticMutexAutoLock& locker,
+ const nsAString& aKey, ScalarBase** aRet);
+
+ bool AllowsKey(const nsAString& aKey) const;
+};
+
+ScalarResult KeyedScalar::SetValue(const StaticMutexAutoLock& locker,
+ const nsAString& aKey, nsIVariant* aValue) {
+ ScalarBase* scalar = nullptr;
+ ScalarResult sr = GetScalarForKey(locker, aKey, &scalar);
+ if (sr != ScalarResult::Ok) {
+ return sr;
+ }
+
+ return scalar->SetValue(aValue);
+}
+
+ScalarResult KeyedScalar::AddValue(const StaticMutexAutoLock& locker,
+ const nsAString& aKey, nsIVariant* aValue) {
+ ScalarBase* scalar = nullptr;
+ ScalarResult sr = GetScalarForKey(locker, aKey, &scalar);
+ if (sr != ScalarResult::Ok) {
+ return sr;
+ }
+
+ return scalar->AddValue(aValue);
+}
+
+ScalarResult KeyedScalar::SetMaximum(const StaticMutexAutoLock& locker,
+ const nsAString& aKey,
+ nsIVariant* aValue) {
+ ScalarBase* scalar = nullptr;
+ ScalarResult sr = GetScalarForKey(locker, aKey, &scalar);
+ if (sr != ScalarResult::Ok) {
+ return sr;
+ }
+
+ return scalar->SetMaximum(aValue);
+}
+
+void KeyedScalar::SetValue(const StaticMutexAutoLock& locker,
+ const nsAString& aKey, uint32_t aValue) {
+ ScalarBase* scalar = nullptr;
+ ScalarResult sr = GetScalarForKey(locker, aKey, &scalar);
+
+ if (sr != ScalarResult::Ok) {
+ // Bug 1451813 - We now report which scalars exceed the key limit in
+ // telemetry.keyed_scalars_exceed_limit.
+ if (sr == ScalarResult::KeyTooLong) {
+ MOZ_ASSERT(false, "Key too long to be recorded in the scalar.");
+ }
+ return;
+ }
+
+ return scalar->SetValue(aValue);
+}
+
+void KeyedScalar::SetValue(const StaticMutexAutoLock& locker,
+ const nsAString& aKey, bool aValue) {
+ ScalarBase* scalar = nullptr;
+ ScalarResult sr = GetScalarForKey(locker, aKey, &scalar);
+
+ if (sr != ScalarResult::Ok) {
+ // Bug 1451813 - We now report which scalars exceed the key limit in
+ // telemetry.keyed_scalars_exceed_limit.
+ if (sr == ScalarResult::KeyTooLong) {
+ MOZ_ASSERT(false, "Key too long to be recorded in the scalar.");
+ }
+ return;
+ }
+
+ return scalar->SetValue(aValue);
+}
+
+void KeyedScalar::AddValue(const StaticMutexAutoLock& locker,
+ const nsAString& aKey, uint32_t aValue) {
+ ScalarBase* scalar = nullptr;
+ ScalarResult sr = GetScalarForKey(locker, aKey, &scalar);
+
+ if (sr != ScalarResult::Ok) {
+ // Bug 1451813 - We now report which scalars exceed the key limit in
+ // telemetry.keyed_scalars_exceed_limit.
+ if (sr == ScalarResult::KeyTooLong) {
+ MOZ_ASSERT(false, "Key too long to be recorded in the scalar.");
+ }
+ return;
+ }
+
+ return scalar->AddValue(aValue);
+}
+
+void KeyedScalar::SetMaximum(const StaticMutexAutoLock& locker,
+ const nsAString& aKey, uint32_t aValue) {
+ ScalarBase* scalar = nullptr;
+ ScalarResult sr = GetScalarForKey(locker, aKey, &scalar);
+
+ if (sr != ScalarResult::Ok) {
+ // Bug 1451813 - We now report which scalars exceed the key limit in
+ // telemetry.keyed_scalars_exceed_limit.
+ if (sr == ScalarResult::KeyTooLong) {
+ MOZ_ASSERT(false, "Key too long to be recorded in the scalar.");
+ }
+
+ return;
+ }
+
+ return scalar->SetMaximum(aValue);
+}
+
+/**
+ * Get a key-value array with the values for the Keyed Scalar.
+ * @param aValue The array that will hold the key-value pairs.
+ * @return {nsresult} NS_OK or an error value as reported by the
+ * the specific scalar objects implementations (e.g.
+ * ScalarUnsigned).
+ */
+nsresult KeyedScalar::GetValue(const nsACString& aStoreName, bool aClearStorage,
+ nsTArray<KeyValuePair>& aValues) {
+ for (const auto& entry : mScalarKeys) {
+ ScalarBase* scalar = entry.GetWeak();
+
+ // Get the scalar value.
+ nsCOMPtr<nsIVariant> scalarValue;
+ nsresult rv = scalar->GetValue(aStoreName, aClearStorage, scalarValue);
+ if (rv == NS_ERROR_NO_CONTENT) {
+ // No value for this store.
+ continue;
+ }
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+
+ // Append it to value list.
+ aValues.AppendElement(
+ std::make_pair(nsCString(entry.GetKey()), scalarValue));
+ }
+
+ return NS_OK;
+}
+
+// Forward declaration
+nsresult internal_GetKeyedScalarByEnum(const StaticMutexAutoLock& lock,
+ const ScalarKey& aId,
+ ProcessID aProcessStorage,
+ KeyedScalar** aRet);
+
+// Forward declaration
+nsresult internal_GetEnumByScalarName(const StaticMutexAutoLock& lock,
+ const nsACString& aName, ScalarKey* aId);
+
+/**
+ * Get the scalar for the referenced key.
+ * If there's no such key, instantiate a new Scalar object with the
+ * same type of the Keyed scalar and create the key.
+ */
+ScalarResult KeyedScalar::GetScalarForKey(const StaticMutexAutoLock& locker,
+ const nsAString& aKey,
+ ScalarBase** aRet) {
+ if (aKey.IsEmpty()) {
+ return ScalarResult::KeyIsEmpty;
+ }
+
+ if (!AllowsKey(aKey)) {
+ KeyedScalar* scalarUnknown = nullptr;
+ ScalarKey scalarUnknownUniqueId{
+ static_cast<uint32_t>(
+ mozilla::Telemetry::ScalarID::TELEMETRY_KEYED_SCALARS_UNKNOWN_KEYS),
+ false};
+ ProcessID process = ProcessID::Parent;
+ nsresult rv = internal_GetKeyedScalarByEnum(locker, scalarUnknownUniqueId,
+ process, &scalarUnknown);
+ if (NS_FAILED(rv)) {
+ return ScalarResult::TooManyKeys;
+ }
+ scalarUnknown->AddValue(locker, NS_ConvertUTF8toUTF16(mScalarName), 1);
+
+ return ScalarResult::KeyNotAllowed;
+ }
+
+ if (aKey.Length() > kMaximumKeyStringLength) {
+ return ScalarResult::KeyTooLong;
+ }
+
+ NS_ConvertUTF16toUTF8 utf8Key(aKey);
+
+ ScalarBase* scalar = nullptr;
+ if (mScalarKeys.Get(utf8Key, &scalar)) {
+ *aRet = scalar;
+ return ScalarResult::Ok;
+ }
+
+ ScalarKey uniqueId;
+ nsresult rv = internal_GetEnumByScalarName(locker, mScalarName, &uniqueId);
+ if (NS_FAILED(rv)) {
+ return (rv == NS_ERROR_FAILURE) ? ScalarResult::NotInitialized
+ : ScalarResult::UnknownScalar;
+ }
+
+ const BaseScalarInfo& info = internal_GetScalarInfo(locker, uniqueId);
+ if (mScalarKeys.Count() >= mMaximumNumberOfKeys) {
+ if (aKey.EqualsLiteral("telemetry.keyed_scalars_exceed_limit")) {
+ return ScalarResult::TooManyKeys;
+ }
+
+ KeyedScalar* scalarExceed = nullptr;
+
+ ScalarKey uniqueId{
+ static_cast<uint32_t>(
+ mozilla::Telemetry::ScalarID::TELEMETRY_KEYED_SCALARS_EXCEED_LIMIT),
+ false};
+
+ ProcessID process = ProcessID::Parent;
+ nsresult rv =
+ internal_GetKeyedScalarByEnum(locker, uniqueId, process, &scalarExceed);
+
+ if (NS_FAILED(rv)) {
+ return ScalarResult::TooManyKeys;
+ }
+
+ scalarExceed->AddValue(locker, NS_ConvertUTF8toUTF16(info.name()), 1);
+
+ return ScalarResult::TooManyKeys;
+ }
+
+ scalar = internal_ScalarAllocate(info);
+ if (!scalar) {
+ return ScalarResult::InvalidType;
+ }
+
+ mScalarKeys.InsertOrUpdate(utf8Key, UniquePtr<ScalarBase>(scalar));
+
+ *aRet = scalar;
+ return ScalarResult::Ok;
+}
+
+size_t KeyedScalar::SizeOfIncludingThis(mozilla::MallocSizeOf aMallocSizeOf) {
+ size_t n = aMallocSizeOf(this);
+ for (const auto& scalar : mScalarKeys.Values()) {
+ n += scalar->SizeOfIncludingThis(aMallocSizeOf);
+ }
+ return n;
+}
+
+bool KeyedScalar::AllowsKey(const nsAString& aKey) const {
+ // If we didn't specify a list of allowed keys, just return true.
+ if (mScalarKeyCount == 0) {
+ return true;
+ }
+
+ for (uint32_t i = 0; i < mScalarKeyCount; ++i) {
+ uint32_t stringIndex = gScalarKeysTable[mScalarKeyOffset + i];
+ if (aKey.EqualsASCII(&gScalarsStringTable[stringIndex])) {
+ return true;
+ }
+ }
+
+ return false;
+}
+
+typedef nsUint32HashKey ScalarIDHashKey;
+typedef nsUint32HashKey ProcessIDHashKey;
+typedef nsClassHashtable<ScalarIDHashKey, ScalarBase> ScalarStorageMapType;
+typedef nsClassHashtable<ScalarIDHashKey, KeyedScalar>
+ KeyedScalarStorageMapType;
+typedef nsClassHashtable<ProcessIDHashKey, ScalarStorageMapType>
+ ProcessesScalarsMapType;
+typedef nsClassHashtable<ProcessIDHashKey, KeyedScalarStorageMapType>
+ ProcessesKeyedScalarsMapType;
+
+typedef std::tuple<const char*, nsCOMPtr<nsIVariant>, uint32_t> ScalarDataTuple;
+typedef nsTArray<ScalarDataTuple> ScalarTupleArray;
+typedef nsTHashMap<ProcessIDHashKey, ScalarTupleArray> ScalarSnapshotTable;
+
+typedef std::tuple<const char*, nsTArray<KeyedScalar::KeyValuePair>, uint32_t>
+ KeyedScalarDataTuple;
+typedef nsTArray<KeyedScalarDataTuple> KeyedScalarTupleArray;
+typedef nsTHashMap<ProcessIDHashKey, KeyedScalarTupleArray>
+ KeyedScalarSnapshotTable;
+
+} // namespace
+
+////////////////////////////////////////////////////////////////////////
+////////////////////////////////////////////////////////////////////////
+//
+// PRIVATE STATE, SHARED BY ALL THREADS
+
+namespace {
+
+// Set to true once this global state has been initialized.
+bool gInitDone = false;
+
+bool gCanRecordBase;
+bool gCanRecordExtended;
+
+// The Name -> ID cache map.
+ScalarMapType gScalarNameIDMap(kScalarCount);
+
+// The (Process Id -> (Scalar ID -> Scalar Object)) map. This is a
+// nsClassHashtable, it owns the scalar instances and takes care of deallocating
+// them when they are removed from the map.
+ProcessesScalarsMapType gScalarStorageMap;
+// As above, for the keyed scalars.
+ProcessesKeyedScalarsMapType gKeyedScalarStorageMap;
+// Provide separate storage for "dynamic builtin" plain and keyed scalars,
+// needed to support "build faster" in local developer builds.
+ProcessesScalarsMapType gDynamicBuiltinScalarStorageMap;
+ProcessesKeyedScalarsMapType gDynamicBuiltinKeyedScalarStorageMap;
+
+// Whether or not the deserialization of persisted scalars is still in progress.
+// This is never the case on Desktop or Fennec.
+// Only GeckoView restores persisted scalars.
+bool gIsDeserializing = false;
+// This batches scalar accumulations that should be applied once loading
+// finished.
+StaticAutoPtr<nsTArray<ScalarAction>> gScalarsActions;
+StaticAutoPtr<nsTArray<KeyedScalarAction>> gKeyedScalarsActions;
+
+bool internal_IsScalarDeserializing(const StaticMutexAutoLock& lock) {
+ return gIsDeserializing;
+}
+
+} // namespace
+
+////////////////////////////////////////////////////////////////////////
+////////////////////////////////////////////////////////////////////////
+//
+// PRIVATE: Function that may call JS code.
+
+// NOTE: the functions in this section all run without protection from
+// |gTelemetryScalarsMutex|. If they held the mutex, there would be the
+// possibility of deadlock because the JS_ calls that they make may call
+// back into the TelemetryScalar interface, hence trying to re-acquire the
+// mutex.
+//
+// This means that these functions potentially race against threads, but
+// that seems preferable to risking deadlock.
+
+namespace {
+
+/**
+ * Converts the error code to a human readable error message and prints it to
+ * the browser console.
+ *
+ * @param aScalarName The name of the scalar that raised the error.
+ * @param aSr The error code.
+ */
+void internal_LogScalarError(const nsACString& aScalarName, ScalarResult aSr) {
+ nsAutoString errorMessage;
+ AppendUTF8toUTF16(aScalarName, errorMessage);
+
+ switch (aSr) {
+ case ScalarResult::NotInitialized:
+ errorMessage.AppendLiteral(u" - Telemetry was not yet initialized.");
+ break;
+ case ScalarResult::CannotUnpackVariant:
+ errorMessage.AppendLiteral(
+ u" - Cannot convert the provided JS value to nsIVariant.");
+ break;
+ case ScalarResult::CannotRecordInProcess:
+ errorMessage.AppendLiteral(
+ u" - Cannot record the scalar in the current process.");
+ break;
+ case ScalarResult::KeyedTypeMismatch:
+ errorMessage.AppendLiteral(
+ u" - Attempting to manage a keyed scalar as a scalar (or "
+ u"vice-versa).");
+ break;
+ case ScalarResult::UnknownScalar:
+ errorMessage.AppendLiteral(u" - Unknown scalar.");
+ break;
+ case ScalarResult::OperationNotSupported:
+ errorMessage.AppendLiteral(
+ u" - The requested operation is not supported on this scalar.");
+ break;
+ case ScalarResult::InvalidType:
+ errorMessage.AppendLiteral(
+ u" - Attempted to set the scalar to an invalid data type.");
+ break;
+ case ScalarResult::InvalidValue:
+ errorMessage.AppendLiteral(
+ u" - Attempted to set the scalar to an incompatible value.");
+ break;
+ case ScalarResult::StringTooLong:
+ AppendUTF8toUTF16(
+ nsPrintfCString(" - Truncating scalar value to %d characters.",
+ kMaximumStringValueLength),
+ errorMessage);
+ break;
+ case ScalarResult::KeyIsEmpty:
+ errorMessage.AppendLiteral(u" - The key must not be empty.");
+ break;
+ case ScalarResult::KeyTooLong:
+ AppendUTF8toUTF16(
+ nsPrintfCString(" - The key length must be limited to %d characters.",
+ kMaximumKeyStringLength),
+ errorMessage);
+ break;
+ case ScalarResult::KeyNotAllowed:
+ AppendUTF8toUTF16(
+ nsPrintfCString(" - The key is not allowed for this scalar."),
+ errorMessage);
+ break;
+ case ScalarResult::TooManyKeys:
+ AppendUTF8toUTF16(
+ nsPrintfCString(" - Keyed scalars cannot have more than %d keys.",
+ kMaximumNumberOfKeys),
+ errorMessage);
+ break;
+ case ScalarResult::UnsignedNegativeValue:
+ errorMessage.AppendLiteral(
+ u" - Trying to set an unsigned scalar to a negative number.");
+ break;
+ case ScalarResult::UnsignedTruncatedValue:
+ errorMessage.AppendLiteral(u" - Truncating float/double number.");
+ break;
+ default:
+ // Nothing.
+ return;
+ }
+
+ LogToBrowserConsole(nsIScriptError::warningFlag, errorMessage);
+}
+
+} // namespace
+
+////////////////////////////////////////////////////////////////////////
+////////////////////////////////////////////////////////////////////////
+//
+// PRIVATE: helpers for the external interface
+
+namespace {
+
+bool internal_CanRecordBase(const StaticMutexAutoLock& lock) {
+ return gCanRecordBase;
+}
+
+bool internal_CanRecordExtended(const StaticMutexAutoLock& lock) {
+ return gCanRecordExtended;
+}
+
+/**
+ * Check if the given scalar is a keyed scalar.
+ *
+ * @param lock Instance of a lock locking gTelemetryHistogramMutex
+ * @param aId The scalar identifier.
+ * @return true if aId refers to a keyed scalar, false otherwise.
+ */
+bool internal_IsKeyedScalar(const StaticMutexAutoLock& lock,
+ const ScalarKey& aId) {
+ return internal_GetScalarInfo(lock, aId).keyed;
+}
+
+/**
+ * Check if we're allowed to record the given scalar in the current
+ * process.
+ *
+ * @param lock Instance of a lock locking gTelemetryHistogramMutex
+ * @param aId The scalar identifier.
+ * @return true if the scalar is allowed to be recorded in the current process,
+ * false otherwise.
+ */
+bool internal_CanRecordProcess(const StaticMutexAutoLock& lock,
+ const ScalarKey& aId) {
+ const BaseScalarInfo& info = internal_GetScalarInfo(lock, aId);
+ return CanRecordInProcess(info.record_in_processes, XRE_GetProcessType());
+}
+
+bool internal_CanRecordProduct(const StaticMutexAutoLock& lock,
+ const ScalarKey& aId) {
+ const BaseScalarInfo& info = internal_GetScalarInfo(lock, aId);
+ return CanRecordProduct(info.products);
+}
+
+bool internal_CanRecordForScalarID(const StaticMutexAutoLock& lock,
+ const ScalarKey& aId) {
+ // Get the scalar info from the id.
+ const BaseScalarInfo& info = internal_GetScalarInfo(lock, aId);
+
+ // Can we record at all?
+ bool canRecordBase = internal_CanRecordBase(lock);
+ if (!canRecordBase) {
+ return false;
+ }
+
+ bool canRecordDataset = CanRecordDataset(info.dataset, canRecordBase,
+ internal_CanRecordExtended(lock));
+ if (!canRecordDataset) {
+ return false;
+ }
+
+ return true;
+}
+
+/**
+ * Check if we are allowed to record the provided scalar.
+ *
+ * @param lock Instance of a lock locking gTelemetryHistogramMutex
+ * @param aId The scalar identifier.
+ * @param aKeyed Are we attempting to write a keyed scalar?
+ * @param aForce Whether to allow recording even if the probe is not allowed on
+ * the current process.
+ * This must only be true for GeckoView persistence and recorded
+ * actions.
+ * @return ScalarResult::Ok if we can record, an error code otherwise.
+ */
+ScalarResult internal_CanRecordScalar(const StaticMutexAutoLock& lock,
+ const ScalarKey& aId, bool aKeyed,
+ bool aForce = false) {
+ // Make sure that we have a keyed scalar if we are trying to change one.
+ if (internal_IsKeyedScalar(lock, aId) != aKeyed) {
+ return ScalarResult::KeyedTypeMismatch;
+ }
+
+ // Are we allowed to record this scalar based on the current Telemetry
+ // settings?
+ if (!internal_CanRecordForScalarID(lock, aId)) {
+ return ScalarResult::CannotRecordDataset;
+ }
+
+ // Can we record in this process?
+ if (!aForce && !internal_CanRecordProcess(lock, aId)) {
+ return ScalarResult::CannotRecordInProcess;
+ }
+
+ // Can we record on this product?
+ if (!internal_CanRecordProduct(lock, aId)) {
+ return ScalarResult::CannotRecordDataset;
+ }
+
+ return ScalarResult::Ok;
+}
+
+/**
+ * Get the scalar enum id from the scalar name.
+ *
+ * @param lock Instance of a lock locking gTelemetryHistogramMutex
+ * @param aName The scalar name.
+ * @param aId The output variable to contain the enum.
+ * @return
+ * NS_ERROR_FAILURE if this was called before init is completed.
+ * NS_ERROR_INVALID_ARG if the name can't be found in the scalar definitions.
+ * NS_OK if the scalar was found and aId contains a valid enum id.
+ */
+nsresult internal_GetEnumByScalarName(const StaticMutexAutoLock& lock,
+ const nsACString& aName, ScalarKey* aId) {
+ if (!gInitDone) {
+ return NS_ERROR_FAILURE;
+ }
+
+ CharPtrEntryType* entry =
+ gScalarNameIDMap.GetEntry(PromiseFlatCString(aName).get());
+ if (!entry) {
+ return NS_ERROR_INVALID_ARG;
+ }
+ *aId = entry->GetData();
+ return NS_OK;
+}
+
+/**
+ * Get a scalar object by its enum id. This implicitly allocates the scalar
+ * object in the storage if it wasn't previously allocated.
+ *
+ * @param lock Instance of a lock locking gTelemetryHistogramMutex
+ * @param aId The scalar identifier.
+ * @param aProcessStorage This drives the selection of the map to use to store
+ * the scalar data coming from child processes. This is only meaningful
+ * when this function is called in parent process. If that's the case,
+ * if this is not |GeckoProcessType_Default|, the process id is used to
+ * allocate and store the scalars.
+ * @param aRes The output variable that stores scalar object.
+ * @return
+ * NS_ERROR_INVALID_ARG if the scalar id is unknown.
+ * NS_ERROR_NOT_AVAILABLE if the scalar is expired.
+ * NS_OK if the scalar was found. If that's the case, aResult contains a
+ * valid pointer to a scalar type.
+ */
+nsresult internal_GetScalarByEnum(const StaticMutexAutoLock& lock,
+ const ScalarKey& aId,
+ ProcessID aProcessStorage,
+ ScalarBase** aRet) {
+ if (!internal_IsValidId(lock, aId)) {
+ MOZ_ASSERT(false, "Requested a scalar with an invalid id.");
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ const BaseScalarInfo& info = internal_GetScalarInfo(lock, aId);
+
+ // Dynamic scalars fixup: they are always stored in the "dynamic" process,
+ // unless they are part of the "builtin" Firefox probes. Please note that
+ // "dynamic builtin" probes are meant to support "artifact" and "build faster"
+ // builds.
+ if (aId.dynamic && !info.builtin) {
+ aProcessStorage = ProcessID::Dynamic;
+ }
+
+ ScalarBase* scalar = nullptr;
+ // Initialize the scalar storage to the parent storage. This will get
+ // set to the child storage if needed.
+ uint32_t storageId = static_cast<uint32_t>(aProcessStorage);
+
+ // Put dynamic-builtin scalars (used to support "build faster") in a
+ // separate storage.
+ ProcessesScalarsMapType& processStorage =
+ (aId.dynamic && info.builtin) ? gDynamicBuiltinScalarStorageMap
+ : gScalarStorageMap;
+
+ // Get the process-specific storage or create one if it's not
+ // available.
+ ScalarStorageMapType* const scalarStorage =
+ processStorage.GetOrInsertNew(storageId);
+
+ // Check if the scalar is already allocated in the parent or in the child
+ // storage.
+ if (scalarStorage->Get(aId.id, &scalar)) {
+ // Dynamic scalars can expire at any time during the session (e.g. an
+ // add-on was updated). Check if it expired.
+ if (aId.dynamic) {
+ const DynamicScalarInfo& dynInfo =
+ static_cast<const DynamicScalarInfo&>(info);
+ if (dynInfo.mDynamicExpiration) {
+ // The Dynamic scalar is expired.
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+ }
+ // This was not a dynamic scalar or was not expired.
+ *aRet = scalar;
+ return NS_OK;
+ }
+
+ // The scalar storage wasn't already allocated. Check if the scalar is expired
+ // and then allocate the storage, if needed.
+ if (IsExpiredVersion(info.expiration())) {
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+
+ scalar = internal_ScalarAllocate(info);
+ if (!scalar) {
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ scalarStorage->InsertOrUpdate(aId.id, UniquePtr<ScalarBase>(scalar));
+ *aRet = scalar;
+ return NS_OK;
+}
+
+void internal_ApplyPendingOperations(const StaticMutexAutoLock& lock);
+
+/**
+ * Record the given action on a scalar into the pending actions list.
+ *
+ * If the pending actions list overflows the high water mark length
+ * all operations are immediately applied, including the passed action.
+ *
+ * @param aScalarAction The action to record.
+ */
+void internal_RecordScalarAction(const StaticMutexAutoLock& lock,
+ const ScalarAction& aScalarAction) {
+ // Make sure to have the storage.
+ if (!gScalarsActions) {
+ gScalarsActions = new nsTArray<ScalarAction>();
+ }
+
+ // Store the action.
+ gScalarsActions->AppendElement(aScalarAction);
+
+ // If this action overflows the pending actions array, we immediately apply
+ // pending operations and assume loading is over. If loading still happens
+ // afterwards, some scalar values might be overwritten and inconsistent, but
+ // we won't lose operations on otherwise untouched probes.
+ if (gScalarsActions->Length() > kScalarActionsArrayHighWaterMark) {
+ internal_ApplyPendingOperations(lock);
+ return;
+ }
+}
+
+/**
+ * Record the given action on a scalar on the main process into the pending
+ * actions list.
+ *
+ * If the pending actions list overflows the high water mark length
+ * all operations are immediately applied, including the passed action.
+ *
+ * @param aId The scalar's ID this action applies to
+ * @param aDynamic Determines if the scalar is dynamic
+ * @param aAction The action to record
+ * @param aValue The additional data for the recorded action
+ */
+void internal_RecordScalarAction(const StaticMutexAutoLock& lock, uint32_t aId,
+ bool aDynamic, ScalarActionType aAction,
+ const ScalarVariant& aValue) {
+ internal_RecordScalarAction(
+ lock,
+ ScalarAction{aId, aDynamic, aAction, Some(aValue), ProcessID::Parent});
+}
+
+/**
+ * Record the given action on a keyed scalar into the pending actions list.
+ *
+ * If the pending actions list overflows the high water mark length
+ * all operations are immediately applied, including the passed action.
+ *
+ * @param aScalarAction The action to record.
+ */
+void internal_RecordKeyedScalarAction(const StaticMutexAutoLock& lock,
+ const KeyedScalarAction& aScalarAction) {
+ // Make sure to have the storage.
+ if (!gKeyedScalarsActions) {
+ gKeyedScalarsActions = new nsTArray<KeyedScalarAction>();
+ }
+
+ // Store the action.
+ gKeyedScalarsActions->AppendElement(aScalarAction);
+
+ // If this action overflows the pending actions array, we immediately apply
+ // pending operations and assume loading is over. If loading still happens
+ // afterwards, some scalar values might be overwritten and inconsistent, but
+ // we won't lose operations on otherwise untouched probes.
+ if (gKeyedScalarsActions->Length() > kScalarActionsArrayHighWaterMark) {
+ internal_ApplyPendingOperations(lock);
+ return;
+ }
+}
+
+/**
+ * Record the given action on a keyed scalar on the main process into the
+ * pending actions list.
+ *
+ * If the pending actions list overflows the high water mark length
+ * all operations are immediately applied, including the passed action.
+ *
+ * @param aId The scalar's ID this action applies to
+ * @param aDynamic Determines if the scalar is dynamic
+ * @param aKey The scalar's key
+ * @param aAction The action to record
+ * @param aValue The additional data for the recorded action
+ */
+void internal_RecordKeyedScalarAction(const StaticMutexAutoLock& lock,
+ uint32_t aId, bool aDynamic,
+ const nsAString& aKey,
+ ScalarActionType aAction,
+ const ScalarVariant& aValue) {
+ internal_RecordKeyedScalarAction(
+ lock,
+ KeyedScalarAction{aId, aDynamic, aAction, NS_ConvertUTF16toUTF8(aKey),
+ Some(aValue), ProcessID::Parent});
+}
+
+/**
+ * Update the scalar with the provided value. This is used by the JS API.
+ *
+ * @param lock Instance of a lock locking gTelemetryHistogramMutex
+ * @param aName The scalar name.
+ * @param aType The action type for updating the scalar.
+ * @param aValue The value to use for updating the scalar.
+ * @param aProcessOverride The process for which the scalar must be updated.
+ * This must only be used for GeckoView persistence. It must be
+ * set to the ProcessID::Parent for all the other cases.
+ * @param aForce Whether to force updating even if load is in progress.
+ * @return a ScalarResult error value.
+ */
+ScalarResult internal_UpdateScalar(
+ const StaticMutexAutoLock& lock, const nsACString& aName,
+ ScalarActionType aType, nsIVariant* aValue,
+ ProcessID aProcessOverride = ProcessID::Parent, bool aForce = false) {
+ ScalarKey uniqueId;
+ nsresult rv = internal_GetEnumByScalarName(lock, aName, &uniqueId);
+ if (NS_FAILED(rv)) {
+ return (rv == NS_ERROR_FAILURE) ? ScalarResult::NotInitialized
+ : ScalarResult::UnknownScalar;
+ }
+
+ ScalarResult sr = internal_CanRecordScalar(lock, uniqueId, false, aForce);
+ if (sr != ScalarResult::Ok) {
+ if (sr == ScalarResult::CannotRecordDataset) {
+ return ScalarResult::Ok;
+ }
+ return sr;
+ }
+
+ // Accumulate in the child process if needed.
+ if (!XRE_IsParentProcess()) {
+ const BaseScalarInfo& info = internal_GetScalarInfo(lock, uniqueId);
+ // Convert the nsIVariant to a Variant.
+ mozilla::Maybe<ScalarVariant> variantValue;
+ sr = GetVariantFromIVariant(aValue, info.kind, variantValue);
+ if (sr != ScalarResult::Ok) {
+ MOZ_ASSERT(false, "Unable to convert nsIVariant to mozilla::Variant.");
+ return sr;
+ }
+ TelemetryIPCAccumulator::RecordChildScalarAction(
+ uniqueId.id, uniqueId.dynamic, aType, variantValue.ref());
+ return ScalarResult::Ok;
+ }
+
+ if (!aForce && internal_IsScalarDeserializing(lock)) {
+ const BaseScalarInfo& info = internal_GetScalarInfo(lock, uniqueId);
+ // Convert the nsIVariant to a Variant.
+ mozilla::Maybe<ScalarVariant> variantValue;
+ sr = GetVariantFromIVariant(aValue, info.kind, variantValue);
+ if (sr != ScalarResult::Ok) {
+ MOZ_ASSERT(false, "Unable to convert nsIVariant to mozilla::Variant.");
+ return sr;
+ }
+ internal_RecordScalarAction(lock, uniqueId.id, uniqueId.dynamic, aType,
+ variantValue.ref());
+ return ScalarResult::Ok;
+ }
+
+ // Finally get the scalar.
+ ScalarBase* scalar = nullptr;
+ rv = internal_GetScalarByEnum(lock, uniqueId, aProcessOverride, &scalar);
+ if (NS_FAILED(rv)) {
+ // Don't throw on expired scalars.
+ if (rv == NS_ERROR_NOT_AVAILABLE) {
+ return ScalarResult::Ok;
+ }
+ return ScalarResult::UnknownScalar;
+ }
+
+ if (aType == ScalarActionType::eAdd) {
+ return scalar->AddValue(aValue);
+ }
+ if (aType == ScalarActionType::eSet) {
+ return scalar->SetValue(aValue);
+ }
+
+ return scalar->SetMaximum(aValue);
+}
+
+} // namespace
+
+////////////////////////////////////////////////////////////////////////
+////////////////////////////////////////////////////////////////////////
+//
+// PRIVATE: thread-unsafe helpers for the keyed scalars
+
+namespace {
+
+/**
+ * Get a keyed scalar object by its enum id. This implicitly allocates the keyed
+ * scalar object in the storage if it wasn't previously allocated.
+ *
+ * @param lock Instance of a lock locking gTelemetryHistogramMutex
+ * @param aId The scalar identifier.
+ * @param aProcessStorage This drives the selection of the map to use to store
+ * the scalar data coming from child processes. This is only meaningful
+ * when this function is called in parent process. If that's the case,
+ * if this is not |GeckoProcessType_Default|, the process id is used to
+ * allocate and store the scalars.
+ * @param aRet The output variable that stores scalar object.
+ * @return
+ * NS_ERROR_INVALID_ARG if the scalar id is unknown or a this is a keyed
+ * string scalar.
+ * NS_ERROR_NOT_AVAILABLE if the scalar is expired.
+ * NS_OK if the scalar was found. If that's the case, aResult contains a
+ * valid pointer to a scalar type.
+ */
+nsresult internal_GetKeyedScalarByEnum(const StaticMutexAutoLock& lock,
+ const ScalarKey& aId,
+ ProcessID aProcessStorage,
+ KeyedScalar** aRet) {
+ if (!internal_IsValidId(lock, aId)) {
+ MOZ_ASSERT(false, "Requested a keyed scalar with an invalid id.");
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ const BaseScalarInfo& info = internal_GetScalarInfo(lock, aId);
+
+ // Dynamic scalars fixup: they are always stored in the "dynamic" process,
+ // unless they are part of the "builtin" Firefox probes. Please note that
+ // "dynamic builtin" probes are meant to support "artifact" and "build faster"
+ // builds.
+ if (aId.dynamic && !info.builtin) {
+ aProcessStorage = ProcessID::Dynamic;
+ }
+
+ KeyedScalar* scalar = nullptr;
+ // Initialize the scalar storage to the parent storage. This will get
+ // set to the child storage if needed.
+ uint32_t storageId = static_cast<uint32_t>(aProcessStorage);
+
+ // Put dynamic-builtin scalars (used to support "build faster") in a
+ // separate storage.
+ ProcessesKeyedScalarsMapType& processStorage =
+ (aId.dynamic && info.builtin) ? gDynamicBuiltinKeyedScalarStorageMap
+ : gKeyedScalarStorageMap;
+
+ // Get the process-specific storage or create one if it's not
+ // available.
+ KeyedScalarStorageMapType* const scalarStorage =
+ processStorage.GetOrInsertNew(storageId);
+
+ if (scalarStorage->Get(aId.id, &scalar)) {
+ *aRet = scalar;
+ return NS_OK;
+ }
+
+ if (IsExpiredVersion(info.expiration())) {
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+
+ // We don't currently support keyed string scalars. Disable them.
+ if (info.kind == nsITelemetry::SCALAR_TYPE_STRING) {
+ MOZ_ASSERT(false, "Keyed string scalars are not currently supported.");
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ scalar = new KeyedScalar(info);
+
+ scalarStorage->InsertOrUpdate(aId.id, UniquePtr<KeyedScalar>(scalar));
+ *aRet = scalar;
+ return NS_OK;
+}
+
+/**
+ * Update the keyed scalar with the provided value. This is used by the JS API.
+ *
+ * @param lock Instance of a lock locking gTelemetryHistogramMutex
+ * @param aName The scalar name.
+ * @param aKey The key name.
+ * @param aType The action type for updating the scalar.
+ * @param aValue The value to use for updating the scalar.
+ * @param aProcessOverride The process for which the scalar must be updated.
+ * This must only be used for GeckoView persistence. It must be
+ * set to the ProcessID::Parent for all the other cases.
+ * @return a ScalarResult error value.
+ */
+ScalarResult internal_UpdateKeyedScalar(
+ const StaticMutexAutoLock& lock, const nsACString& aName,
+ const nsAString& aKey, ScalarActionType aType, nsIVariant* aValue,
+ ProcessID aProcessOverride = ProcessID::Parent, bool aForce = false) {
+ ScalarKey uniqueId;
+ nsresult rv = internal_GetEnumByScalarName(lock, aName, &uniqueId);
+ if (NS_FAILED(rv)) {
+ return (rv == NS_ERROR_FAILURE) ? ScalarResult::NotInitialized
+ : ScalarResult::UnknownScalar;
+ }
+
+ ScalarResult sr = internal_CanRecordScalar(lock, uniqueId, true, aForce);
+ if (sr != ScalarResult::Ok) {
+ if (sr == ScalarResult::CannotRecordDataset) {
+ return ScalarResult::Ok;
+ }
+ return sr;
+ }
+
+ // Accumulate in the child process if needed.
+ if (!XRE_IsParentProcess()) {
+ const BaseScalarInfo& info = internal_GetScalarInfo(lock, uniqueId);
+ // Convert the nsIVariant to a Variant.
+ mozilla::Maybe<ScalarVariant> variantValue;
+ sr = GetVariantFromIVariant(aValue, info.kind, variantValue);
+ if (sr != ScalarResult::Ok) {
+ MOZ_ASSERT(false, "Unable to convert nsIVariant to mozilla::Variant.");
+ return sr;
+ }
+ TelemetryIPCAccumulator::RecordChildKeyedScalarAction(
+ uniqueId.id, uniqueId.dynamic, aKey, aType, variantValue.ref());
+ return ScalarResult::Ok;
+ }
+
+ if (!aForce && internal_IsScalarDeserializing(lock)) {
+ const BaseScalarInfo& info = internal_GetScalarInfo(lock, uniqueId);
+ // Convert the nsIVariant to a Variant.
+ mozilla::Maybe<ScalarVariant> variantValue;
+ sr = GetVariantFromIVariant(aValue, info.kind, variantValue);
+ if (sr != ScalarResult::Ok) {
+ MOZ_ASSERT(false, "Unable to convert nsIVariant to mozilla::Variant.");
+ return sr;
+ }
+ internal_RecordKeyedScalarAction(lock, uniqueId.id, uniqueId.dynamic, aKey,
+ aType, variantValue.ref());
+ return ScalarResult::Ok;
+ }
+
+ // Finally get the scalar.
+ KeyedScalar* scalar = nullptr;
+ rv = internal_GetKeyedScalarByEnum(lock, uniqueId, aProcessOverride, &scalar);
+ if (NS_FAILED(rv)) {
+ // Don't throw on expired scalars.
+ if (rv == NS_ERROR_NOT_AVAILABLE) {
+ return ScalarResult::Ok;
+ }
+ return ScalarResult::UnknownScalar;
+ }
+
+ if (aType == ScalarActionType::eAdd) {
+ return scalar->AddValue(lock, aKey, aValue);
+ }
+ if (aType == ScalarActionType::eSet) {
+ return scalar->SetValue(lock, aKey, aValue);
+ }
+
+ return scalar->SetMaximum(lock, aKey, aValue);
+}
+
+/**
+ * Helper function to convert an array of |DynamicScalarInfo|
+ * to |DynamicScalarDefinition| used by the IPC calls.
+ */
+void internal_DynamicScalarToIPC(
+ const StaticMutexAutoLock& lock,
+ const nsTArray<DynamicScalarInfo>& aDynamicScalarInfos,
+ nsTArray<DynamicScalarDefinition>& aIPCDefs) {
+ for (auto& info : aDynamicScalarInfos) {
+ DynamicScalarDefinition stubDefinition;
+ stubDefinition.type = info.kind;
+ stubDefinition.dataset = info.dataset;
+ stubDefinition.expired = info.mDynamicExpiration;
+ stubDefinition.keyed = info.keyed;
+ stubDefinition.name = info.mDynamicName;
+ stubDefinition.builtin = info.builtin;
+ aIPCDefs.AppendElement(stubDefinition);
+ }
+}
+
+/**
+ * Broadcasts the dynamic scalar definitions to all the other
+ * content processes.
+ */
+void internal_BroadcastDefinitions(
+ const nsTArray<DynamicScalarDefinition>& scalarDefs) {
+ nsTArray<mozilla::dom::ContentParent*> parents;
+ mozilla::dom::ContentParent::GetAll(parents);
+ if (!parents.Length()) {
+ return;
+ }
+
+ // Broadcast the definitions to the other content processes.
+ for (auto parent : parents) {
+ mozilla::Unused << parent->SendAddDynamicScalars(scalarDefs);
+ }
+}
+
+void internal_RegisterScalars(const StaticMutexAutoLock& lock,
+ const nsTArray<DynamicScalarInfo>& scalarInfos) {
+ // Register the new scalars.
+ if (!gDynamicScalarInfo) {
+ gDynamicScalarInfo = new nsTArray<DynamicScalarInfo>();
+ }
+ if (!gDynamicStoreNames) {
+ gDynamicStoreNames = new nsTArray<RefPtr<nsAtom>>();
+ }
+
+ for (auto& scalarInfo : scalarInfos) {
+ // Allow expiring scalars that were already registered.
+ CharPtrEntryType* existingKey =
+ gScalarNameIDMap.GetEntry(scalarInfo.name());
+ if (existingKey) {
+ // Change the scalar to expired if needed.
+ if (scalarInfo.mDynamicExpiration && !scalarInfo.builtin) {
+ DynamicScalarInfo& scalarData =
+ (*gDynamicScalarInfo)[existingKey->GetData().id];
+ scalarData.mDynamicExpiration = true;
+ }
+ continue;
+ }
+
+ gDynamicScalarInfo->AppendElement(scalarInfo);
+ uint32_t scalarId = gDynamicScalarInfo->Length() - 1;
+ CharPtrEntryType* entry = gScalarNameIDMap.PutEntry(scalarInfo.name());
+ entry->SetData(ScalarKey{scalarId, true});
+ }
+}
+
+/**
+ * Creates a snapshot of the desired scalar storage.
+ * @param {aLock} The proof of lock to access scalar data.
+ * @param {aScalarsToReflect} The table that will contain the snapshot.
+ * @param {aDataset} The dataset we're asking the snapshot for.
+ * @param {aProcessStorage} The scalar storage to take a snapshot of.
+ * @param {aIsBuiltinDynamic} Whether or not the storage is for dynamic builtin
+ * scalars.
+ * @return NS_OK or the error code describing the failure reason.
+ */
+nsresult internal_ScalarSnapshotter(const StaticMutexAutoLock& aLock,
+ ScalarSnapshotTable& aScalarsToReflect,
+ unsigned int aDataset,
+ ProcessesScalarsMapType& aProcessStorage,
+ bool aIsBuiltinDynamic, bool aClearScalars,
+ const nsACString& aStoreName) {
+ // Iterate the scalars in aProcessStorage. The storage may contain empty or
+ // yet to be initialized scalars from all the supported processes.
+ for (const auto& entry : aProcessStorage) {
+ ScalarStorageMapType* scalarStorage = entry.GetWeak();
+ ScalarTupleArray& processScalars =
+ aScalarsToReflect.LookupOrInsert(entry.GetKey());
+
+ // Are we in the "Dynamic" process?
+ bool isDynamicProcess =
+ ProcessID::Dynamic == static_cast<ProcessID>(entry.GetKey());
+
+ // Iterate each available child storage.
+ for (const auto& childEntry : *scalarStorage) {
+ ScalarBase* scalar = childEntry.GetWeak();
+
+ // Get the informations for this scalar.
+ const BaseScalarInfo& info = internal_GetScalarInfo(
+ aLock, ScalarKey{childEntry.GetKey(),
+ aIsBuiltinDynamic ? true : isDynamicProcess});
+
+ // Serialize the scalar if it's in the desired dataset.
+ if (IsInDataset(info.dataset, aDataset)) {
+ // Get the scalar value.
+ nsCOMPtr<nsIVariant> scalarValue;
+ nsresult rv = scalar->GetValue(aStoreName, aClearScalars, scalarValue);
+ if (rv == NS_ERROR_NO_CONTENT) {
+ // No value for this store. Proceed.
+ continue;
+ }
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+ // Append it to our list.
+ processScalars.AppendElement(
+ std::make_tuple(info.name(), scalarValue, info.kind));
+ }
+ }
+ if (processScalars.Length() == 0) {
+ aScalarsToReflect.Remove(entry.GetKey());
+ }
+ }
+ return NS_OK;
+}
+
+/**
+ * Creates a snapshot of the desired keyed scalar storage.
+ * @param {aLock} The proof of lock to access scalar data.
+ * @param {aScalarsToReflect} The table that will contain the snapshot.
+ * @param {aDataset} The dataset we're asking the snapshot for.
+ * @param {aProcessStorage} The scalar storage to take a snapshot of.
+ * @param {aIsBuiltinDynamic} Whether or not the storage is for dynamic builtin
+ * scalars.
+ * @return NS_OK or the error code describing the failure reason.
+ */
+nsresult internal_KeyedScalarSnapshotter(
+ const StaticMutexAutoLock& aLock,
+ KeyedScalarSnapshotTable& aScalarsToReflect, unsigned int aDataset,
+ ProcessesKeyedScalarsMapType& aProcessStorage, bool aIsBuiltinDynamic,
+ bool aClearScalars, const nsACString& aStoreName) {
+ // Iterate the scalars in aProcessStorage. The storage may contain empty or
+ // yet to be initialized scalars from all the supported processes.
+ for (const auto& entry : aProcessStorage) {
+ KeyedScalarStorageMapType* scalarStorage = entry.GetWeak();
+ KeyedScalarTupleArray& processScalars =
+ aScalarsToReflect.LookupOrInsert(entry.GetKey());
+
+ // Are we in the "Dynamic" process?
+ bool isDynamicProcess =
+ ProcessID::Dynamic == static_cast<ProcessID>(entry.GetKey());
+
+ for (const auto& childEntry : *scalarStorage) {
+ KeyedScalar* scalar = childEntry.GetWeak();
+
+ // Get the informations for this scalar.
+ const BaseScalarInfo& info = internal_GetScalarInfo(
+ aLock, ScalarKey{childEntry.GetKey(),
+ aIsBuiltinDynamic ? true : isDynamicProcess});
+
+ // Serialize the scalar if it's in the desired dataset.
+ if (IsInDataset(info.dataset, aDataset)) {
+ // Get the keys for this scalar.
+ nsTArray<KeyedScalar::KeyValuePair> scalarKeyedData;
+ nsresult rv =
+ scalar->GetValue(aStoreName, aClearScalars, scalarKeyedData);
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+ if (scalarKeyedData.Length() == 0) {
+ // Don't bother with empty keyed scalars.
+ continue;
+ }
+ // Append it to our list.
+ processScalars.AppendElement(std::make_tuple(
+ info.name(), std::move(scalarKeyedData), info.kind));
+ }
+ }
+ if (processScalars.Length() == 0) {
+ aScalarsToReflect.Remove(entry.GetKey());
+ }
+ }
+ return NS_OK;
+}
+
+/**
+ * Helper function to get a snapshot of the scalars.
+ *
+ * @param {aLock} The proof of lock to access scalar data.
+ * @param {aScalarsToReflect} The table that will contain the snapshot.
+ * @param {aDataset} The dataset we're asking the snapshot for.
+ * @param {aClearScalars} Whether or not to clear the scalar storage.
+ * @param {aStoreName} The name of the store to snapshot.
+ * @return NS_OK or the error code describing the failure reason.
+ */
+nsresult internal_GetScalarSnapshot(const StaticMutexAutoLock& aLock,
+ ScalarSnapshotTable& aScalarsToReflect,
+ unsigned int aDataset, bool aClearScalars,
+ const nsACString& aStoreName) {
+ // Take a snapshot of the scalars.
+ nsresult rv =
+ internal_ScalarSnapshotter(aLock, aScalarsToReflect, aDataset,
+ gScalarStorageMap, false, /*aIsBuiltinDynamic*/
+ aClearScalars, aStoreName);
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+
+ // And a snapshot of the dynamic builtin ones.
+ rv = internal_ScalarSnapshotter(aLock, aScalarsToReflect, aDataset,
+ gDynamicBuiltinScalarStorageMap,
+ true, /*aIsBuiltinDynamic*/
+ aClearScalars, aStoreName);
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+
+ return NS_OK;
+}
+
+/**
+ * Helper function to get a snapshot of the keyed scalars.
+ *
+ * @param {aLock} The proof of lock to access scalar data.
+ * @param {aScalarsToReflect} The table that will contain the snapshot.
+ * @param {aDataset} The dataset we're asking the snapshot for.
+ * @param {aClearScalars} Whether or not to clear the scalar storage.
+ * @param {aStoreName} The name of the store to snapshot.
+ * @return NS_OK or the error code describing the failure reason.
+ */
+nsresult internal_GetKeyedScalarSnapshot(
+ const StaticMutexAutoLock& aLock,
+ KeyedScalarSnapshotTable& aScalarsToReflect, unsigned int aDataset,
+ bool aClearScalars, const nsACString& aStoreName) {
+ // Take a snapshot of the scalars.
+ nsresult rv = internal_KeyedScalarSnapshotter(
+ aLock, aScalarsToReflect, aDataset, gKeyedScalarStorageMap,
+ false, /*aIsBuiltinDynamic*/
+ aClearScalars, aStoreName);
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+
+ // And a snapshot of the dynamic builtin ones.
+ rv = internal_KeyedScalarSnapshotter(aLock, aScalarsToReflect, aDataset,
+ gDynamicBuiltinKeyedScalarStorageMap,
+ true, /*aIsBuiltinDynamic*/
+ aClearScalars, aStoreName);
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+
+ return NS_OK;
+}
+
+} // namespace
+
+// helpers for recording/applying scalar operations
+namespace {
+
+void internal_ApplyScalarActions(
+ const StaticMutexAutoLock& lock,
+ const nsTArray<mozilla::Telemetry::ScalarAction>& aScalarActions,
+ const mozilla::Maybe<ProcessID>& aProcessType = Nothing()) {
+ if (!internal_CanRecordBase(lock)) {
+ return;
+ }
+
+ for (auto& upd : aScalarActions) {
+ ScalarKey uniqueId{upd.mId, upd.mDynamic};
+ if (NS_WARN_IF(!internal_IsValidId(lock, uniqueId))) {
+ MOZ_ASSERT_UNREACHABLE("Scalar usage requires valid ids.");
+ continue;
+ }
+
+ if (internal_IsKeyedScalar(lock, uniqueId)) {
+ continue;
+ }
+
+ // Are we allowed to record this scalar? We don't need to check for
+ // allowed processes here, that's taken care of when recording
+ // in child processes.
+ if (!internal_CanRecordForScalarID(lock, uniqueId)) {
+ continue;
+ }
+
+ // Either we got passed a process type or it was explicitely set on the
+ // recorded action. It should never happen that it is set to an invalid
+ // value (such as ProcessID::Count)
+ ProcessID processType = aProcessType.valueOr(upd.mProcessType);
+ MOZ_ASSERT(processType != ProcessID::Count);
+
+ // Refresh the data in the parent process with the data coming from the
+ // child processes.
+ ScalarBase* scalar = nullptr;
+ nsresult rv =
+ internal_GetScalarByEnum(lock, uniqueId, processType, &scalar);
+ if (NS_FAILED(rv)) {
+ // Bug 1513496 - We no longer log a warning if the scalar is expired.
+ if (rv != NS_ERROR_NOT_AVAILABLE) {
+ NS_WARNING("NS_FAILED internal_GetScalarByEnum for CHILD");
+ }
+ continue;
+ }
+
+ if (upd.mData.isNothing()) {
+ MOZ_ASSERT(false, "There is no data in the ScalarActionType.");
+ continue;
+ }
+
+ // Get the type of this scalar from the scalar ID. We already checked
+ // for its validity a few lines above.
+ const uint32_t scalarType = internal_GetScalarInfo(lock, uniqueId).kind;
+
+ // Extract the data from the mozilla::Variant.
+ switch (upd.mActionType) {
+ case ScalarActionType::eSet: {
+ switch (scalarType) {
+ case nsITelemetry::SCALAR_TYPE_COUNT:
+ if (!upd.mData->is<uint32_t>()) {
+ NS_WARNING("Attempting to set a count scalar to a non-integer.");
+ continue;
+ }
+ scalar->SetValue(upd.mData->as<uint32_t>());
+ break;
+ case nsITelemetry::SCALAR_TYPE_BOOLEAN:
+ if (!upd.mData->is<bool>()) {
+ NS_WARNING(
+ "Attempting to set a boolean scalar to a non-boolean.");
+ continue;
+ }
+ scalar->SetValue(upd.mData->as<bool>());
+ break;
+ case nsITelemetry::SCALAR_TYPE_STRING:
+ if (!upd.mData->is<nsString>()) {
+ NS_WARNING("Attempting to set a string scalar to a non-string.");
+ continue;
+ }
+ scalar->SetValue(upd.mData->as<nsString>());
+ break;
+ }
+ break;
+ }
+ case ScalarActionType::eAdd: {
+ if (scalarType != nsITelemetry::SCALAR_TYPE_COUNT) {
+ NS_WARNING("Attempting to add on a non count scalar.");
+ continue;
+ }
+ // We only support adding uint32_t.
+ if (!upd.mData->is<uint32_t>()) {
+ NS_WARNING("Attempting to add to a count scalar with a non-integer.");
+ continue;
+ }
+ scalar->AddValue(upd.mData->as<uint32_t>());
+ break;
+ }
+ case ScalarActionType::eSetMaximum: {
+ if (scalarType != nsITelemetry::SCALAR_TYPE_COUNT) {
+ NS_WARNING("Attempting to setMaximum on a non count scalar.");
+ continue;
+ }
+ // We only support SetMaximum on uint32_t.
+ if (!upd.mData->is<uint32_t>()) {
+ NS_WARNING(
+ "Attempting to setMaximum a count scalar to a non-integer.");
+ continue;
+ }
+ scalar->SetMaximum(upd.mData->as<uint32_t>());
+ break;
+ }
+ default:
+ NS_WARNING("Unsupported action coming from scalar child updates.");
+ }
+ }
+}
+
+void internal_ApplyKeyedScalarActions(
+ const StaticMutexAutoLock& lock,
+ const nsTArray<mozilla::Telemetry::KeyedScalarAction>& aScalarActions,
+ const mozilla::Maybe<ProcessID>& aProcessType = Nothing()) {
+ if (!internal_CanRecordBase(lock)) {
+ return;
+ }
+
+ for (auto& upd : aScalarActions) {
+ ScalarKey uniqueId{upd.mId, upd.mDynamic};
+ if (NS_WARN_IF(!internal_IsValidId(lock, uniqueId))) {
+ MOZ_ASSERT_UNREACHABLE("Scalar usage requires valid ids.");
+ continue;
+ }
+
+ if (!internal_IsKeyedScalar(lock, uniqueId)) {
+ continue;
+ }
+
+ // Are we allowed to record this scalar? We don't need to check for
+ // allowed processes here, that's taken care of when recording
+ // in child processes.
+ if (!internal_CanRecordForScalarID(lock, uniqueId)) {
+ continue;
+ }
+
+ // Either we got passed a process type or it was explicitely set on the
+ // recorded action. It should never happen that it is set to an invalid
+ // value (such as ProcessID::Count)
+ ProcessID processType = aProcessType.valueOr(upd.mProcessType);
+ MOZ_ASSERT(processType != ProcessID::Count);
+
+ // Refresh the data in the parent process with the data coming from the
+ // child processes.
+ KeyedScalar* scalar = nullptr;
+ nsresult rv =
+ internal_GetKeyedScalarByEnum(lock, uniqueId, processType, &scalar);
+ if (NS_FAILED(rv)) {
+ // Bug 1513496 - We no longer log a warning if the scalar is expired.
+ if (rv != NS_ERROR_NOT_AVAILABLE) {
+ NS_WARNING("NS_FAILED internal_GetKeyedScalarByEnum for CHILD");
+ }
+ continue;
+ }
+
+ if (upd.mData.isNothing()) {
+ MOZ_ASSERT(false, "There is no data in the KeyedScalarAction.");
+ continue;
+ }
+
+ // Get the type of this scalar from the scalar ID. We already checked
+ // for its validity a few lines above.
+ const uint32_t scalarType = internal_GetScalarInfo(lock, uniqueId).kind;
+
+ // Extract the data from the mozilla::Variant.
+ switch (upd.mActionType) {
+ case ScalarActionType::eSet: {
+ switch (scalarType) {
+ case nsITelemetry::SCALAR_TYPE_COUNT:
+ if (!upd.mData->is<uint32_t>()) {
+ NS_WARNING("Attempting to set a count scalar to a non-integer.");
+ continue;
+ }
+ scalar->SetValue(lock, NS_ConvertUTF8toUTF16(upd.mKey),
+ upd.mData->as<uint32_t>());
+ break;
+ case nsITelemetry::SCALAR_TYPE_BOOLEAN:
+ if (!upd.mData->is<bool>()) {
+ NS_WARNING(
+ "Attempting to set a boolean scalar to a non-boolean.");
+ continue;
+ }
+ scalar->SetValue(lock, NS_ConvertUTF8toUTF16(upd.mKey),
+ upd.mData->as<bool>());
+ break;
+ default:
+ NS_WARNING("Unsupported type coming from scalar child updates.");
+ }
+ break;
+ }
+ case ScalarActionType::eAdd: {
+ if (scalarType != nsITelemetry::SCALAR_TYPE_COUNT) {
+ NS_WARNING("Attempting to add on a non count scalar.");
+ continue;
+ }
+ // We only support adding on uint32_t.
+ if (!upd.mData->is<uint32_t>()) {
+ NS_WARNING("Attempting to add to a count scalar with a non-integer.");
+ continue;
+ }
+ scalar->AddValue(lock, NS_ConvertUTF8toUTF16(upd.mKey),
+ upd.mData->as<uint32_t>());
+ break;
+ }
+ case ScalarActionType::eSetMaximum: {
+ if (scalarType != nsITelemetry::SCALAR_TYPE_COUNT) {
+ NS_WARNING("Attempting to setMaximum on a non count scalar.");
+ continue;
+ }
+ // We only support SetMaximum on uint32_t.
+ if (!upd.mData->is<uint32_t>()) {
+ NS_WARNING(
+ "Attempting to setMaximum a count scalar to a non-integer.");
+ continue;
+ }
+ scalar->SetMaximum(lock, NS_ConvertUTF8toUTF16(upd.mKey),
+ upd.mData->as<uint32_t>());
+ break;
+ }
+ default:
+ NS_WARNING(
+ "Unsupported action coming from keyed scalar child updates.");
+ }
+ }
+}
+
+void internal_ApplyPendingOperations(const StaticMutexAutoLock& lock) {
+ if (gScalarsActions && gScalarsActions->Length() > 0) {
+ internal_ApplyScalarActions(lock, *gScalarsActions);
+ gScalarsActions->Clear();
+ }
+
+ if (gKeyedScalarsActions && gKeyedScalarsActions->Length() > 0) {
+ internal_ApplyKeyedScalarActions(lock, *gKeyedScalarsActions);
+ gKeyedScalarsActions->Clear();
+ }
+
+ // After all pending operations are applied deserialization is done
+ gIsDeserializing = false;
+}
+
+} // namespace
+
+////////////////////////////////////////////////////////////////////////
+////////////////////////////////////////////////////////////////////////
+//
+// EXTERNALLY VISIBLE FUNCTIONS in namespace TelemetryScalars::
+
+// This is a StaticMutex rather than a plain Mutex (1) so that
+// it gets initialised in a thread-safe manner the first time
+// it is used, and (2) because it is never de-initialised, and
+// a normal Mutex would show up as a leak in BloatView. StaticMutex
+// also has the "OffTheBooks" property, so it won't show as a leak
+// in BloatView.
+// Another reason to use a StaticMutex instead of a plain Mutex is
+// that, due to the nature of Telemetry, we cannot rely on having a
+// mutex initialized in InitializeGlobalState. Unfortunately, we
+// cannot make sure that no other function is called before this point.
+static StaticMutex gTelemetryScalarsMutex MOZ_UNANNOTATED;
+
+void TelemetryScalar::InitializeGlobalState(bool aCanRecordBase,
+ bool aCanRecordExtended) {
+ StaticMutexAutoLock locker(gTelemetryScalarsMutex);
+ MOZ_ASSERT(!gInitDone,
+ "TelemetryScalar::InitializeGlobalState "
+ "may only be called once");
+
+ gCanRecordBase = aCanRecordBase;
+ gCanRecordExtended = aCanRecordExtended;
+
+ // Populate the static scalar name->id cache. Note that the scalar names are
+ // statically allocated and come from the automatically generated
+ // TelemetryScalarData.h.
+ uint32_t scalarCount =
+ static_cast<uint32_t>(mozilla::Telemetry::ScalarID::ScalarCount);
+ for (uint32_t i = 0; i < scalarCount; i++) {
+ CharPtrEntryType* entry = gScalarNameIDMap.PutEntry(gScalars[i].name());
+ entry->SetData(ScalarKey{i, false});
+ }
+
+ // To summarize dynamic events we need a dynamic scalar.
+ const nsTArray<DynamicScalarInfo> initialDynamicScalars({
+ DynamicScalarInfo{
+ nsITelemetry::SCALAR_TYPE_COUNT,
+ true /* recordOnRelease */,
+ false /* expired */,
+ nsAutoCString("telemetry.dynamic_event_counts"),
+ true /* keyed */,
+ false /* built-in */,
+ {} /* stores */,
+ },
+ });
+ internal_RegisterScalars(locker, initialDynamicScalars);
+
+ gInitDone = true;
+}
+
+void TelemetryScalar::DeInitializeGlobalState() {
+ StaticMutexAutoLock locker(gTelemetryScalarsMutex);
+ gCanRecordBase = false;
+ gCanRecordExtended = false;
+ gScalarNameIDMap.Clear();
+ gScalarStorageMap.Clear();
+ gKeyedScalarStorageMap.Clear();
+ gDynamicBuiltinScalarStorageMap.Clear();
+ gDynamicBuiltinKeyedScalarStorageMap.Clear();
+ gDynamicScalarInfo = nullptr;
+ gDynamicStoreNames = nullptr;
+ gInitDone = false;
+}
+
+void TelemetryScalar::DeserializationStarted() {
+ StaticMutexAutoLock locker(gTelemetryScalarsMutex);
+ gIsDeserializing = true;
+}
+
+void TelemetryScalar::ApplyPendingOperations() {
+ StaticMutexAutoLock locker(gTelemetryScalarsMutex);
+ internal_ApplyPendingOperations(locker);
+}
+
+void TelemetryScalar::SetCanRecordBase(bool b) {
+ StaticMutexAutoLock locker(gTelemetryScalarsMutex);
+ gCanRecordBase = b;
+}
+
+void TelemetryScalar::SetCanRecordExtended(bool b) {
+ StaticMutexAutoLock locker(gTelemetryScalarsMutex);
+ gCanRecordExtended = b;
+}
+
+/**
+ * Adds the value to the given scalar.
+ *
+ * @param aName The scalar name.
+ * @param aVal The numeric value to add to the scalar.
+ * @param aCx The JS context.
+ * @return NS_OK (always) so that the JS API call doesn't throw. In case of
+ * errors, a warning level message is printed in the browser console.
+ */
+nsresult TelemetryScalar::Add(const nsACString& aName,
+ JS::Handle<JS::Value> aVal, JSContext* aCx) {
+ // Unpack the aVal to nsIVariant. This uses the JS context.
+ nsCOMPtr<nsIVariant> unpackedVal;
+ nsresult rv = nsContentUtils::XPConnect()->JSToVariant(
+ aCx, aVal, getter_AddRefs(unpackedVal));
+ if (NS_FAILED(rv)) {
+ internal_LogScalarError(aName, ScalarResult::CannotUnpackVariant);
+ return NS_OK;
+ }
+
+ ScalarResult sr;
+ {
+ StaticMutexAutoLock locker(gTelemetryScalarsMutex);
+ sr = internal_UpdateScalar(locker, aName, ScalarActionType::eAdd,
+ unpackedVal);
+ }
+
+ // Warn the user about the error if we need to.
+ if (sr != ScalarResult::Ok) {
+ internal_LogScalarError(aName, sr);
+ }
+
+ return NS_OK;
+}
+
+/**
+ * Adds the value to the given scalar.
+ *
+ * @param aName The scalar name.
+ * @param aKey The key name.
+ * @param aVal The numeric value to add to the scalar.
+ * @param aCx The JS context.
+ * @return NS_OK (always) so that the JS API call doesn't throw. In case of
+ * errors, a warning level message is printed in the browser console.
+ */
+nsresult TelemetryScalar::Add(const nsACString& aName, const nsAString& aKey,
+ JS::Handle<JS::Value> aVal, JSContext* aCx) {
+ // Unpack the aVal to nsIVariant. This uses the JS context.
+ nsCOMPtr<nsIVariant> unpackedVal;
+ nsresult rv = nsContentUtils::XPConnect()->JSToVariant(
+ aCx, aVal, getter_AddRefs(unpackedVal));
+ if (NS_FAILED(rv)) {
+ internal_LogScalarError(aName, ScalarResult::CannotUnpackVariant);
+ return NS_OK;
+ }
+
+ ScalarResult sr;
+ {
+ StaticMutexAutoLock locker(gTelemetryScalarsMutex);
+ sr = internal_UpdateKeyedScalar(locker, aName, aKey, ScalarActionType::eAdd,
+ unpackedVal);
+ }
+
+ // Warn the user about the error if we need to.
+ if (sr != ScalarResult::Ok) {
+ internal_LogScalarError(aName, sr);
+ }
+
+ return NS_OK;
+}
+
+/**
+ * Adds the value to the given scalar.
+ *
+ * @param aId The scalar enum id.
+ * @param aVal The numeric value to add to the scalar.
+ */
+void TelemetryScalar::Add(mozilla::Telemetry::ScalarID aId, uint32_t aValue) {
+ if (NS_WARN_IF(!IsValidEnumId(aId))) {
+ MOZ_ASSERT_UNREACHABLE("Scalar usage requires valid ids.");
+ return;
+ }
+
+ ScalarKey uniqueId{static_cast<uint32_t>(aId), false};
+ StaticMutexAutoLock locker(gTelemetryScalarsMutex);
+
+ if (internal_CanRecordScalar(locker, uniqueId, false) != ScalarResult::Ok) {
+ // We can't record this scalar. Bail out.
+ return;
+ }
+
+ // Accumulate in the child process if needed.
+ if (!XRE_IsParentProcess()) {
+ TelemetryIPCAccumulator::RecordChildScalarAction(
+ uniqueId.id, uniqueId.dynamic, ScalarActionType::eAdd,
+ ScalarVariant(aValue));
+ return;
+ }
+
+ if (internal_IsScalarDeserializing(locker)) {
+ internal_RecordScalarAction(locker, uniqueId.id, uniqueId.dynamic,
+ ScalarActionType::eAdd, ScalarVariant(aValue));
+ return;
+ }
+
+ ScalarBase* scalar = nullptr;
+ nsresult rv =
+ internal_GetScalarByEnum(locker, uniqueId, ProcessID::Parent, &scalar);
+ if (NS_FAILED(rv)) {
+ return;
+ }
+
+ scalar->AddValue(aValue);
+}
+
+/**
+ * Adds the value to the given keyed scalar.
+ *
+ * @param aId The scalar enum id.
+ * @param aKey The key name.
+ * @param aVal The numeric value to add to the scalar.
+ */
+void TelemetryScalar::Add(mozilla::Telemetry::ScalarID aId,
+ const nsAString& aKey, uint32_t aValue) {
+ if (NS_WARN_IF(!IsValidEnumId(aId))) {
+ MOZ_ASSERT_UNREACHABLE("Scalar usage requires valid ids.");
+ return;
+ }
+
+ ScalarKey uniqueId{static_cast<uint32_t>(aId), false};
+ StaticMutexAutoLock locker(gTelemetryScalarsMutex);
+
+ if (internal_CanRecordScalar(locker, uniqueId, true) != ScalarResult::Ok) {
+ // We can't record this scalar. Bail out.
+ return;
+ }
+
+ // Accumulate in the child process if needed.
+ if (!XRE_IsParentProcess()) {
+ TelemetryIPCAccumulator::RecordChildKeyedScalarAction(
+ uniqueId.id, uniqueId.dynamic, aKey, ScalarActionType::eAdd,
+ ScalarVariant(aValue));
+ return;
+ }
+
+ if (internal_IsScalarDeserializing(locker)) {
+ internal_RecordKeyedScalarAction(locker, uniqueId.id, uniqueId.dynamic,
+ aKey, ScalarActionType::eAdd,
+ ScalarVariant(aValue));
+ return;
+ }
+
+ KeyedScalar* scalar = nullptr;
+ nsresult rv = internal_GetKeyedScalarByEnum(locker, uniqueId,
+ ProcessID::Parent, &scalar);
+ if (NS_FAILED(rv)) {
+ return;
+ }
+
+ scalar->AddValue(locker, aKey, aValue);
+}
+
+/**
+ * Sets the scalar to the given value.
+ *
+ * @param aName The scalar name.
+ * @param aVal The value to set the scalar to.
+ * @param aCx The JS context.
+ * @return NS_OK (always) so that the JS API call doesn't throw. In case of
+ * errors, a warning level message is printed in the browser console.
+ */
+nsresult TelemetryScalar::Set(const nsACString& aName,
+ JS::Handle<JS::Value> aVal, JSContext* aCx) {
+ // Unpack the aVal to nsIVariant. This uses the JS context.
+ nsCOMPtr<nsIVariant> unpackedVal;
+ nsresult rv = nsContentUtils::XPConnect()->JSToVariant(
+ aCx, aVal, getter_AddRefs(unpackedVal));
+ if (NS_FAILED(rv)) {
+ internal_LogScalarError(aName, ScalarResult::CannotUnpackVariant);
+ return NS_OK;
+ }
+
+ ScalarResult sr;
+ {
+ StaticMutexAutoLock locker(gTelemetryScalarsMutex);
+ sr = internal_UpdateScalar(locker, aName, ScalarActionType::eSet,
+ unpackedVal);
+ }
+
+ // Warn the user about the error if we need to.
+ if (sr != ScalarResult::Ok) {
+ internal_LogScalarError(aName, sr);
+ }
+
+ return NS_OK;
+}
+
+/**
+ * Sets the keyed scalar to the given value.
+ *
+ * @param aName The scalar name.
+ * @param aKey The key name.
+ * @param aVal The value to set the scalar to.
+ * @param aCx The JS context.
+ * @return NS_OK (always) so that the JS API call doesn't throw. In case of
+ * errors, a warning level message is printed in the browser console.
+ */
+nsresult TelemetryScalar::Set(const nsACString& aName, const nsAString& aKey,
+ JS::Handle<JS::Value> aVal, JSContext* aCx) {
+ // Unpack the aVal to nsIVariant. This uses the JS context.
+ nsCOMPtr<nsIVariant> unpackedVal;
+ nsresult rv = nsContentUtils::XPConnect()->JSToVariant(
+ aCx, aVal, getter_AddRefs(unpackedVal));
+ if (NS_FAILED(rv)) {
+ internal_LogScalarError(aName, ScalarResult::CannotUnpackVariant);
+ return NS_OK;
+ }
+
+ ScalarResult sr;
+ {
+ StaticMutexAutoLock locker(gTelemetryScalarsMutex);
+ sr = internal_UpdateKeyedScalar(locker, aName, aKey, ScalarActionType::eSet,
+ unpackedVal);
+ }
+
+ // Warn the user about the error if we need to.
+ if (sr != ScalarResult::Ok) {
+ internal_LogScalarError(aName, sr);
+ }
+
+ return NS_OK;
+}
+
+/**
+ * Sets the scalar to the given numeric value.
+ *
+ * @param aId The scalar enum id.
+ * @param aValue The numeric, unsigned value to set the scalar to.
+ */
+void TelemetryScalar::Set(mozilla::Telemetry::ScalarID aId, uint32_t aValue) {
+ if (NS_WARN_IF(!IsValidEnumId(aId))) {
+ MOZ_ASSERT_UNREACHABLE("Scalar usage requires valid ids.");
+ return;
+ }
+
+ ScalarKey uniqueId{static_cast<uint32_t>(aId), false};
+ StaticMutexAutoLock locker(gTelemetryScalarsMutex);
+
+ if (internal_CanRecordScalar(locker, uniqueId, false) != ScalarResult::Ok) {
+ // We can't record this scalar. Bail out.
+ return;
+ }
+
+ // Accumulate in the child process if needed.
+ if (!XRE_IsParentProcess()) {
+ TelemetryIPCAccumulator::RecordChildScalarAction(
+ uniqueId.id, uniqueId.dynamic, ScalarActionType::eSet,
+ ScalarVariant(aValue));
+ return;
+ }
+
+ if (internal_IsScalarDeserializing(locker)) {
+ internal_RecordScalarAction(locker, uniqueId.id, uniqueId.dynamic,
+ ScalarActionType::eSet, ScalarVariant(aValue));
+ return;
+ }
+
+ ScalarBase* scalar = nullptr;
+ nsresult rv =
+ internal_GetScalarByEnum(locker, uniqueId, ProcessID::Parent, &scalar);
+ if (NS_FAILED(rv)) {
+ return;
+ }
+
+ scalar->SetValue(aValue);
+}
+
+/**
+ * Sets the scalar to the given string value.
+ *
+ * @param aId The scalar enum id.
+ * @param aValue The string value to set the scalar to.
+ */
+void TelemetryScalar::Set(mozilla::Telemetry::ScalarID aId,
+ const nsAString& aValue) {
+ if (NS_WARN_IF(!IsValidEnumId(aId))) {
+ MOZ_ASSERT_UNREACHABLE("Scalar usage requires valid ids.");
+ return;
+ }
+
+ ScalarKey uniqueId{static_cast<uint32_t>(aId), false};
+ StaticMutexAutoLock locker(gTelemetryScalarsMutex);
+
+ if (internal_CanRecordScalar(locker, uniqueId, false) != ScalarResult::Ok) {
+ // We can't record this scalar. Bail out.
+ return;
+ }
+
+ // Accumulate in the child process if needed.
+ if (!XRE_IsParentProcess()) {
+ TelemetryIPCAccumulator::RecordChildScalarAction(
+ uniqueId.id, uniqueId.dynamic, ScalarActionType::eSet,
+ ScalarVariant(nsString(aValue)));
+ return;
+ }
+
+ if (internal_IsScalarDeserializing(locker)) {
+ internal_RecordScalarAction(locker, uniqueId.id, uniqueId.dynamic,
+ ScalarActionType::eSet,
+ ScalarVariant(nsString(aValue)));
+ return;
+ }
+
+ ScalarBase* scalar = nullptr;
+ nsresult rv =
+ internal_GetScalarByEnum(locker, uniqueId, ProcessID::Parent, &scalar);
+ if (NS_FAILED(rv)) {
+ return;
+ }
+
+ scalar->SetValue(aValue);
+}
+
+/**
+ * Sets the scalar to the given boolean value.
+ *
+ * @param aId The scalar enum id.
+ * @param aValue The boolean value to set the scalar to.
+ */
+void TelemetryScalar::Set(mozilla::Telemetry::ScalarID aId, bool aValue) {
+ if (NS_WARN_IF(!IsValidEnumId(aId))) {
+ MOZ_ASSERT_UNREACHABLE("Scalar usage requires valid ids.");
+ return;
+ }
+
+ ScalarKey uniqueId{static_cast<uint32_t>(aId), false};
+ StaticMutexAutoLock locker(gTelemetryScalarsMutex);
+
+ if (internal_CanRecordScalar(locker, uniqueId, false) != ScalarResult::Ok) {
+ // We can't record this scalar. Bail out.
+ return;
+ }
+
+ // Accumulate in the child process if needed.
+ if (!XRE_IsParentProcess()) {
+ TelemetryIPCAccumulator::RecordChildScalarAction(
+ uniqueId.id, uniqueId.dynamic, ScalarActionType::eSet,
+ ScalarVariant(aValue));
+ return;
+ }
+
+ if (internal_IsScalarDeserializing(locker)) {
+ internal_RecordScalarAction(locker, uniqueId.id, uniqueId.dynamic,
+ ScalarActionType::eSet, ScalarVariant(aValue));
+ return;
+ }
+
+ ScalarBase* scalar = nullptr;
+ nsresult rv =
+ internal_GetScalarByEnum(locker, uniqueId, ProcessID::Parent, &scalar);
+ if (NS_FAILED(rv)) {
+ return;
+ }
+
+ scalar->SetValue(aValue);
+}
+
+/**
+ * Sets the keyed scalar to the given numeric value.
+ *
+ * @param aId The scalar enum id.
+ * @param aKey The scalar key.
+ * @param aValue The numeric, unsigned value to set the scalar to.
+ */
+void TelemetryScalar::Set(mozilla::Telemetry::ScalarID aId,
+ const nsAString& aKey, uint32_t aValue) {
+ if (NS_WARN_IF(!IsValidEnumId(aId))) {
+ MOZ_ASSERT_UNREACHABLE("Scalar usage requires valid ids.");
+ return;
+ }
+
+ ScalarKey uniqueId{static_cast<uint32_t>(aId), false};
+ StaticMutexAutoLock locker(gTelemetryScalarsMutex);
+
+ if (internal_CanRecordScalar(locker, uniqueId, true) != ScalarResult::Ok) {
+ // We can't record this scalar. Bail out.
+ return;
+ }
+
+ // Accumulate in the child process if needed.
+ if (!XRE_IsParentProcess()) {
+ TelemetryIPCAccumulator::RecordChildKeyedScalarAction(
+ uniqueId.id, uniqueId.dynamic, aKey, ScalarActionType::eSet,
+ ScalarVariant(aValue));
+ return;
+ }
+
+ if (internal_IsScalarDeserializing(locker)) {
+ internal_RecordKeyedScalarAction(locker, uniqueId.id, uniqueId.dynamic,
+ aKey, ScalarActionType::eSet,
+ ScalarVariant(aValue));
+ return;
+ }
+
+ KeyedScalar* scalar = nullptr;
+ nsresult rv = internal_GetKeyedScalarByEnum(locker, uniqueId,
+ ProcessID::Parent, &scalar);
+ if (NS_FAILED(rv)) {
+ return;
+ }
+
+ scalar->SetValue(locker, aKey, aValue);
+}
+
+/**
+ * Sets the scalar to the given boolean value.
+ *
+ * @param aId The scalar enum id.
+ * @param aKey The scalar key.
+ * @param aValue The boolean value to set the scalar to.
+ */
+void TelemetryScalar::Set(mozilla::Telemetry::ScalarID aId,
+ const nsAString& aKey, bool aValue) {
+ if (NS_WARN_IF(!IsValidEnumId(aId))) {
+ MOZ_ASSERT_UNREACHABLE("Scalar usage requires valid ids.");
+ return;
+ }
+
+ ScalarKey uniqueId{static_cast<uint32_t>(aId), false};
+ StaticMutexAutoLock locker(gTelemetryScalarsMutex);
+
+ if (internal_CanRecordScalar(locker, uniqueId, true) != ScalarResult::Ok) {
+ // We can't record this scalar. Bail out.
+ return;
+ }
+
+ // Accumulate in the child process if needed.
+ if (!XRE_IsParentProcess()) {
+ TelemetryIPCAccumulator::RecordChildKeyedScalarAction(
+ uniqueId.id, uniqueId.dynamic, aKey, ScalarActionType::eSet,
+ ScalarVariant(aValue));
+ return;
+ }
+
+ if (internal_IsScalarDeserializing(locker)) {
+ internal_RecordKeyedScalarAction(locker, uniqueId.id, uniqueId.dynamic,
+ aKey, ScalarActionType::eSet,
+ ScalarVariant(aValue));
+ return;
+ }
+
+ KeyedScalar* scalar = nullptr;
+ nsresult rv = internal_GetKeyedScalarByEnum(locker, uniqueId,
+ ProcessID::Parent, &scalar);
+ if (NS_FAILED(rv)) {
+ return;
+ }
+
+ scalar->SetValue(locker, aKey, aValue);
+}
+
+/**
+ * Sets the scalar to the maximum of the current and the passed value.
+ *
+ * @param aName The scalar name.
+ * @param aVal The numeric value to set the scalar to.
+ * @param aCx The JS context.
+ * @return NS_OK (always) so that the JS API call doesn't throw. In case of
+ * errors, a warning level message is printed in the browser console.
+ */
+nsresult TelemetryScalar::SetMaximum(const nsACString& aName,
+ JS::Handle<JS::Value> aVal,
+ JSContext* aCx) {
+ // Unpack the aVal to nsIVariant. This uses the JS context.
+ nsCOMPtr<nsIVariant> unpackedVal;
+ nsresult rv = nsContentUtils::XPConnect()->JSToVariant(
+ aCx, aVal, getter_AddRefs(unpackedVal));
+ if (NS_FAILED(rv)) {
+ internal_LogScalarError(aName, ScalarResult::CannotUnpackVariant);
+ return NS_OK;
+ }
+
+ ScalarResult sr;
+ {
+ StaticMutexAutoLock locker(gTelemetryScalarsMutex);
+ sr = internal_UpdateScalar(locker, aName, ScalarActionType::eSetMaximum,
+ unpackedVal);
+ }
+
+ // Warn the user about the error if we need to.
+ if (sr != ScalarResult::Ok) {
+ internal_LogScalarError(aName, sr);
+ }
+
+ return NS_OK;
+}
+
+/**
+ * Sets the scalar to the maximum of the current and the passed value.
+ *
+ * @param aName The scalar name.
+ * @param aKey The key name.
+ * @param aVal The numeric value to set the scalar to.
+ * @param aCx The JS context.
+ * @return NS_OK (always) so that the JS API call doesn't throw. In case of
+ * errors, a warning level message is printed in the browser console.
+ */
+nsresult TelemetryScalar::SetMaximum(const nsACString& aName,
+ const nsAString& aKey,
+ JS::Handle<JS::Value> aVal,
+ JSContext* aCx) {
+ // Unpack the aVal to nsIVariant. This uses the JS context.
+ nsCOMPtr<nsIVariant> unpackedVal;
+ nsresult rv = nsContentUtils::XPConnect()->JSToVariant(
+ aCx, aVal, getter_AddRefs(unpackedVal));
+ if (NS_FAILED(rv)) {
+ internal_LogScalarError(aName, ScalarResult::CannotUnpackVariant);
+ return NS_OK;
+ }
+
+ ScalarResult sr;
+ {
+ StaticMutexAutoLock locker(gTelemetryScalarsMutex);
+ sr = internal_UpdateKeyedScalar(locker, aName, aKey,
+ ScalarActionType::eSetMaximum, unpackedVal);
+ }
+
+ // Warn the user about the error if we need to.
+ if (sr != ScalarResult::Ok) {
+ internal_LogScalarError(aName, sr);
+ }
+
+ return NS_OK;
+}
+
+/**
+ * Sets the scalar to the maximum of the current and the passed value.
+ *
+ * @param aId The scalar enum id.
+ * @param aValue The numeric value to set the scalar to.
+ */
+void TelemetryScalar::SetMaximum(mozilla::Telemetry::ScalarID aId,
+ uint32_t aValue) {
+ if (NS_WARN_IF(!IsValidEnumId(aId))) {
+ MOZ_ASSERT_UNREACHABLE("Scalar usage requires valid ids.");
+ return;
+ }
+
+ ScalarKey uniqueId{static_cast<uint32_t>(aId), false};
+ StaticMutexAutoLock locker(gTelemetryScalarsMutex);
+
+ if (internal_CanRecordScalar(locker, uniqueId, false) != ScalarResult::Ok) {
+ // We can't record this scalar. Bail out.
+ return;
+ }
+
+ // Accumulate in the child process if needed.
+ if (!XRE_IsParentProcess()) {
+ TelemetryIPCAccumulator::RecordChildScalarAction(
+ uniqueId.id, uniqueId.dynamic, ScalarActionType::eSetMaximum,
+ ScalarVariant(aValue));
+ return;
+ }
+
+ if (internal_IsScalarDeserializing(locker)) {
+ internal_RecordScalarAction(locker, uniqueId.id, uniqueId.dynamic,
+ ScalarActionType::eSetMaximum,
+ ScalarVariant(aValue));
+ return;
+ }
+
+ ScalarBase* scalar = nullptr;
+ nsresult rv =
+ internal_GetScalarByEnum(locker, uniqueId, ProcessID::Parent, &scalar);
+ if (NS_FAILED(rv)) {
+ return;
+ }
+
+ scalar->SetMaximum(aValue);
+}
+
+/**
+ * Sets the keyed scalar to the maximum of the current and the passed value.
+ *
+ * @param aId The scalar enum id.
+ * @param aKey The key name.
+ * @param aValue The numeric value to set the scalar to.
+ */
+void TelemetryScalar::SetMaximum(mozilla::Telemetry::ScalarID aId,
+ const nsAString& aKey, uint32_t aValue) {
+ if (NS_WARN_IF(!IsValidEnumId(aId))) {
+ MOZ_ASSERT_UNREACHABLE("Scalar usage requires valid ids.");
+ return;
+ }
+
+ ScalarKey uniqueId{static_cast<uint32_t>(aId), false};
+ StaticMutexAutoLock locker(gTelemetryScalarsMutex);
+
+ if (internal_CanRecordScalar(locker, uniqueId, true) != ScalarResult::Ok) {
+ // We can't record this scalar. Bail out.
+ return;
+ }
+
+ // Accumulate in the child process if needed.
+ if (!XRE_IsParentProcess()) {
+ TelemetryIPCAccumulator::RecordChildKeyedScalarAction(
+ uniqueId.id, uniqueId.dynamic, aKey, ScalarActionType::eSetMaximum,
+ ScalarVariant(aValue));
+ return;
+ }
+
+ if (internal_IsScalarDeserializing(locker)) {
+ internal_RecordKeyedScalarAction(locker, uniqueId.id, uniqueId.dynamic,
+ aKey, ScalarActionType::eSetMaximum,
+ ScalarVariant(aValue));
+ return;
+ }
+
+ KeyedScalar* scalar = nullptr;
+ nsresult rv = internal_GetKeyedScalarByEnum(locker, uniqueId,
+ ProcessID::Parent, &scalar);
+ if (NS_FAILED(rv)) {
+ return;
+ }
+
+ scalar->SetMaximum(locker, aKey, aValue);
+}
+
+nsresult TelemetryScalar::CreateSnapshots(unsigned int aDataset,
+ bool aClearScalars, JSContext* aCx,
+ uint8_t optional_argc,
+ JS::MutableHandle<JS::Value> aResult,
+ bool aFilterTest,
+ const nsACString& aStoreName) {
+ MOZ_ASSERT(
+ XRE_IsParentProcess(),
+ "Snapshotting scalars should only happen in the parent processes.");
+ // If no arguments were passed in, apply the default value.
+ if (!optional_argc) {
+ aClearScalars = false;
+ }
+
+ JS::Rooted<JSObject*> root_obj(aCx, JS_NewPlainObject(aCx));
+ if (!root_obj) {
+ return NS_ERROR_FAILURE;
+ }
+ aResult.setObject(*root_obj);
+
+ // Return `{}` in child processes.
+ if (!XRE_IsParentProcess()) {
+ return NS_OK;
+ }
+
+ // Only lock the mutex while accessing our data, without locking any JS
+ // related code.
+ ScalarSnapshotTable scalarsToReflect;
+ {
+ StaticMutexAutoLock locker(gTelemetryScalarsMutex);
+
+ nsresult rv = internal_GetScalarSnapshot(locker, scalarsToReflect, aDataset,
+ aClearScalars, aStoreName);
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+ }
+
+ // Reflect it to JS.
+ for (const auto& entry : scalarsToReflect) {
+ const ScalarTupleArray& processScalars = entry.GetData();
+ const char* processName = GetNameForProcessID(ProcessID(entry.GetKey()));
+
+ // Create the object that will hold the scalars for this process and add it
+ // to the returned root object.
+ JS::Rooted<JSObject*> processObj(aCx, JS_NewPlainObject(aCx));
+ if (!processObj || !JS_DefineProperty(aCx, root_obj, processName,
+ processObj, JSPROP_ENUMERATE)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ for (ScalarTupleArray::size_type i = 0; i < processScalars.Length(); i++) {
+ const ScalarDataTuple& scalar = processScalars[i];
+
+ const char* scalarName = std::get<0>(scalar);
+ if (aFilterTest && strncmp(TEST_SCALAR_PREFIX, scalarName,
+ strlen(TEST_SCALAR_PREFIX)) == 0) {
+ continue;
+ }
+
+ // Convert it to a JS Val.
+ JS::Rooted<JS::Value> scalarJsValue(aCx);
+ nsresult rv = nsContentUtils::XPConnect()->VariantToJS(
+ aCx, processObj, std::get<1>(scalar), &scalarJsValue);
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+
+ // Add it to the scalar object.
+ if (!JS_DefineProperty(aCx, processObj, scalarName, scalarJsValue,
+ JSPROP_ENUMERATE)) {
+ return NS_ERROR_FAILURE;
+ }
+ }
+ }
+
+ return NS_OK;
+}
+
+nsresult TelemetryScalar::CreateKeyedSnapshots(
+ unsigned int aDataset, bool aClearScalars, JSContext* aCx,
+ uint8_t optional_argc, JS::MutableHandle<JS::Value> aResult,
+ bool aFilterTest, const nsACString& aStoreName) {
+ MOZ_ASSERT(
+ XRE_IsParentProcess(),
+ "Snapshotting scalars should only happen in the parent processes.");
+ // If no arguments were passed in, apply the default value.
+ if (!optional_argc) {
+ aClearScalars = false;
+ }
+
+ JS::Rooted<JSObject*> root_obj(aCx, JS_NewPlainObject(aCx));
+ if (!root_obj) {
+ return NS_ERROR_FAILURE;
+ }
+ aResult.setObject(*root_obj);
+
+ // Return `{}` in child processes.
+ if (!XRE_IsParentProcess()) {
+ return NS_OK;
+ }
+
+ // Only lock the mutex while accessing our data, without locking any JS
+ // related code.
+ KeyedScalarSnapshotTable scalarsToReflect;
+ {
+ StaticMutexAutoLock locker(gTelemetryScalarsMutex);
+
+ nsresult rv = internal_GetKeyedScalarSnapshot(
+ locker, scalarsToReflect, aDataset, aClearScalars, aStoreName);
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+ }
+
+ // Reflect it to JS.
+ for (const auto& entry : scalarsToReflect) {
+ const KeyedScalarTupleArray& processScalars = entry.GetData();
+ const char* processName = GetNameForProcessID(ProcessID(entry.GetKey()));
+
+ // Create the object that will hold the scalars for this process and add it
+ // to the returned root object.
+ JS::Rooted<JSObject*> processObj(aCx, JS_NewPlainObject(aCx));
+ if (!processObj || !JS_DefineProperty(aCx, root_obj, processName,
+ processObj, JSPROP_ENUMERATE)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ for (KeyedScalarTupleArray::size_type i = 0; i < processScalars.Length();
+ i++) {
+ const KeyedScalarDataTuple& keyedScalarData = processScalars[i];
+
+ const char* scalarName = std::get<0>(keyedScalarData);
+ if (aFilterTest && strncmp(TEST_SCALAR_PREFIX, scalarName,
+ strlen(TEST_SCALAR_PREFIX)) == 0) {
+ continue;
+ }
+
+ // Go through each keyed scalar and create a keyed scalar object.
+ // This object will hold the values for all the keyed scalar keys.
+ JS::Rooted<JSObject*> keyedScalarObj(aCx, JS_NewPlainObject(aCx));
+
+ // Define a property for each scalar key, then add it to the keyed scalar
+ // object.
+ const nsTArray<KeyedScalar::KeyValuePair>& keyProps =
+ std::get<1>(keyedScalarData);
+ for (uint32_t i = 0; i < keyProps.Length(); i++) {
+ const KeyedScalar::KeyValuePair& keyData = keyProps[i];
+
+ // Convert the value for the key to a JSValue.
+ JS::Rooted<JS::Value> keyJsValue(aCx);
+ nsresult rv = nsContentUtils::XPConnect()->VariantToJS(
+ aCx, keyedScalarObj, keyData.second, &keyJsValue);
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+
+ // Add the key to the scalar representation.
+ const NS_ConvertUTF8toUTF16 key(keyData.first);
+ if (!JS_DefineUCProperty(aCx, keyedScalarObj, key.Data(), key.Length(),
+ keyJsValue, JSPROP_ENUMERATE)) {
+ return NS_ERROR_FAILURE;
+ }
+ }
+
+ // Add the scalar to the root object.
+ if (!JS_DefineProperty(aCx, processObj, scalarName, keyedScalarObj,
+ JSPROP_ENUMERATE)) {
+ return NS_ERROR_FAILURE;
+ }
+ }
+ }
+
+ return NS_OK;
+}
+
+nsresult TelemetryScalar::RegisterScalars(const nsACString& aCategoryName,
+ JS::Handle<JS::Value> aScalarData,
+ bool aBuiltin, JSContext* cx) {
+ MOZ_ASSERT(XRE_IsParentProcess(),
+ "Dynamic scalars should only be created in the parent process.");
+
+ if (!IsValidIdentifierString(aCategoryName, kMaximumCategoryNameLength, true,
+ false)) {
+ JS_ReportErrorASCII(cx, "Invalid category name %s.",
+ PromiseFlatCString(aCategoryName).get());
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ if (!aScalarData.isObject()) {
+ JS_ReportErrorASCII(cx, "Scalar data parameter should be an object");
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ JS::Rooted<JSObject*> obj(cx, &aScalarData.toObject());
+ JS::Rooted<JS::IdVector> scalarPropertyIds(cx, JS::IdVector(cx));
+ if (!JS_Enumerate(cx, obj, &scalarPropertyIds)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ // Collect the scalar data into local storage first.
+ // Only after successfully validating all contained scalars will we register
+ // them into global storage.
+ nsTArray<DynamicScalarInfo> newScalarInfos;
+
+ for (size_t i = 0, n = scalarPropertyIds.length(); i < n; i++) {
+ nsAutoJSString scalarName;
+ if (!scalarName.init(cx, scalarPropertyIds[i])) {
+ return NS_ERROR_FAILURE;
+ }
+
+ if (!IsValidIdentifierString(NS_ConvertUTF16toUTF8(scalarName),
+ kMaximumScalarNameLength, false, true)) {
+ JS_ReportErrorASCII(
+ cx, "Invalid scalar name %s.",
+ PromiseFlatCString(NS_ConvertUTF16toUTF8(scalarName)).get());
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ // Join the category and the probe names.
+ nsPrintfCString fullName("%s.%s", PromiseFlatCString(aCategoryName).get(),
+ NS_ConvertUTF16toUTF8(scalarName).get());
+
+ JS::Rooted<JS::Value> value(cx);
+ if (!JS_GetPropertyById(cx, obj, scalarPropertyIds[i], &value) ||
+ !value.isObject()) {
+ return NS_ERROR_FAILURE;
+ }
+ JS::Rooted<JSObject*> scalarDef(cx, &value.toObject());
+
+ // Get the scalar's kind.
+ if (!JS_GetProperty(cx, scalarDef, "kind", &value) || !value.isInt32()) {
+ JS_ReportErrorASCII(cx, "Invalid or missing 'kind' for scalar %s.",
+ PromiseFlatCString(fullName).get());
+ return NS_ERROR_FAILURE;
+ }
+ uint32_t kind = static_cast<uint32_t>(value.toInt32());
+
+ // Get the optional scalar's recording policy (default to false).
+ bool hasProperty = false;
+ bool recordOnRelease = false;
+ if (JS_HasProperty(cx, scalarDef, "record_on_release", &hasProperty) &&
+ hasProperty) {
+ if (!JS_GetProperty(cx, scalarDef, "record_on_release", &value) ||
+ !value.isBoolean()) {
+ JS_ReportErrorASCII(cx, "Invalid 'record_on_release' for scalar %s.",
+ PromiseFlatCString(fullName).get());
+ return NS_ERROR_FAILURE;
+ }
+ recordOnRelease = static_cast<bool>(value.toBoolean());
+ }
+
+ // Get the optional scalar's keyed (default to false).
+ bool keyed = false;
+ if (JS_HasProperty(cx, scalarDef, "keyed", &hasProperty) && hasProperty) {
+ if (!JS_GetProperty(cx, scalarDef, "keyed", &value) ||
+ !value.isBoolean()) {
+ JS_ReportErrorASCII(cx, "Invalid 'keyed' for scalar %s.",
+ PromiseFlatCString(fullName).get());
+ return NS_ERROR_FAILURE;
+ }
+ keyed = static_cast<bool>(value.toBoolean());
+ }
+
+ // Get the optional scalar's expired state (default to false).
+ bool expired = false;
+ if (JS_HasProperty(cx, scalarDef, "expired", &hasProperty) && hasProperty) {
+ if (!JS_GetProperty(cx, scalarDef, "expired", &value) ||
+ !value.isBoolean()) {
+ JS_ReportErrorASCII(cx, "Invalid 'expired' for scalar %s.",
+ PromiseFlatCString(fullName).get());
+ return NS_ERROR_FAILURE;
+ }
+ expired = static_cast<bool>(value.toBoolean());
+ }
+
+ // Get the scalar's optional stores list (default to ["main"]).
+ nsTArray<nsCString> stores;
+ if (JS_HasProperty(cx, scalarDef, "stores", &hasProperty) && hasProperty) {
+ bool isArray = false;
+ if (!JS_GetProperty(cx, scalarDef, "stores", &value) ||
+ !JS::IsArrayObject(cx, value, &isArray) || !isArray) {
+ JS_ReportErrorASCII(cx, "Invalid 'stores' for scalar %s.",
+ PromiseFlatCString(fullName).get());
+ return NS_ERROR_FAILURE;
+ }
+
+ JS::Rooted<JSObject*> arrayObj(cx, &value.toObject());
+ uint32_t storesLength = 0;
+ if (!JS::GetArrayLength(cx, arrayObj, &storesLength)) {
+ JS_ReportErrorASCII(cx,
+ "Can't get 'stores' array length for scalar %s.",
+ PromiseFlatCString(fullName).get());
+ return NS_ERROR_FAILURE;
+ }
+
+ for (uint32_t i = 0; i < storesLength; ++i) {
+ JS::Rooted<JS::Value> elt(cx);
+ if (!JS_GetElement(cx, arrayObj, i, &elt)) {
+ JS_ReportErrorASCII(
+ cx, "Can't get element from scalar %s 'stores' array.",
+ PromiseFlatCString(fullName).get());
+ return NS_ERROR_FAILURE;
+ }
+ if (!elt.isString()) {
+ JS_ReportErrorASCII(cx,
+ "Element in scalar %s 'stores' array isn't a "
+ "string.",
+ PromiseFlatCString(fullName).get());
+ return NS_ERROR_FAILURE;
+ }
+
+ nsAutoJSString jsStr;
+ if (!jsStr.init(cx, elt)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ stores.AppendElement(NS_ConvertUTF16toUTF8(jsStr));
+ }
+ // In the event of the usual case (just "main"), save the storage.
+ if (stores.Length() == 1 && stores[0].EqualsLiteral("main")) {
+ stores.TruncateLength(0);
+ }
+ }
+
+ // We defer the actual registration here in case any other event description
+ // is invalid. In that case we don't need to roll back any partial
+ // registration.
+ newScalarInfos.AppendElement(
+ DynamicScalarInfo{kind, recordOnRelease, expired, fullName, keyed,
+ aBuiltin, std::move(stores)});
+ }
+
+ // Register the dynamic definition on the parent process.
+ nsTArray<DynamicScalarDefinition> ipcDefinitions;
+ {
+ StaticMutexAutoLock locker(gTelemetryScalarsMutex);
+ ::internal_RegisterScalars(locker, newScalarInfos);
+
+ // Convert the internal scalar representation to a stripped down IPC one.
+ ::internal_DynamicScalarToIPC(locker, newScalarInfos, ipcDefinitions);
+ }
+
+ // Propagate the registration to all the content-processes.
+ // Do not hold the mutex while calling IPC.
+ ::internal_BroadcastDefinitions(ipcDefinitions);
+
+ return NS_OK;
+}
+
+/**
+ * Count in Scalars how many of which events were recorded. See bug 1440673
+ *
+ * Event Telemetry unfortunately cannot use vanilla ScalarAdd because it needs
+ * to summarize events recorded in different processes to the
+ * telemetry.event_counts of the same process. Including "dynamic".
+ *
+ * @param aUniqueEventName - expected to be category#object#method
+ * @param aProcessType - the process of the event being summarized
+ * @param aDynamic - whether the event being summarized was dynamic
+ */
+void TelemetryScalar::SummarizeEvent(const nsCString& aUniqueEventName,
+ ProcessID aProcessType, bool aDynamic) {
+ MOZ_ASSERT(XRE_IsParentProcess(),
+ "Only summarize events in the parent process");
+ if (!XRE_IsParentProcess()) {
+ return;
+ }
+
+ StaticMutexAutoLock lock(gTelemetryScalarsMutex);
+
+ ScalarKey scalarKey{static_cast<uint32_t>(ScalarID::TELEMETRY_EVENT_COUNTS),
+ aDynamic};
+ if (aDynamic) {
+ nsresult rv = internal_GetEnumByScalarName(
+ lock, nsAutoCString("telemetry.dynamic_event_counts"), &scalarKey);
+ if (NS_FAILED(rv)) {
+ NS_WARNING(
+ "NS_FAILED getting ScalarKey for telemetry.dynamic_event_counts");
+ return;
+ }
+ }
+
+ KeyedScalar* scalar = nullptr;
+ nsresult rv =
+ internal_GetKeyedScalarByEnum(lock, scalarKey, aProcessType, &scalar);
+
+ if (NS_FAILED(rv)) {
+ NS_WARNING("NS_FAILED getting keyed scalar for event summary. Wut.");
+ return;
+ }
+
+ // Set this each time as it may have been cleared and recreated between calls
+ scalar->SetMaximumNumberOfKeys(kMaxEventSummaryKeys);
+
+ scalar->AddValue(lock, NS_ConvertASCIItoUTF16(aUniqueEventName), 1);
+}
+
+/**
+ * Resets all the stored scalars. This is intended to be only used in tests.
+ */
+void TelemetryScalar::ClearScalars() {
+ MOZ_ASSERT(XRE_IsParentProcess(),
+ "Scalars should only be cleared in the parent process.");
+ if (!XRE_IsParentProcess()) {
+ return;
+ }
+
+ StaticMutexAutoLock locker(gTelemetryScalarsMutex);
+ gScalarStorageMap.Clear();
+ gKeyedScalarStorageMap.Clear();
+ gDynamicBuiltinScalarStorageMap.Clear();
+ gDynamicBuiltinKeyedScalarStorageMap.Clear();
+ gScalarsActions = nullptr;
+ gKeyedScalarsActions = nullptr;
+}
+
+size_t TelemetryScalar::GetMapShallowSizesOfExcludingThis(
+ mozilla::MallocSizeOf aMallocSizeOf) {
+ StaticMutexAutoLock locker(gTelemetryScalarsMutex);
+ return gScalarNameIDMap.ShallowSizeOfExcludingThis(aMallocSizeOf);
+}
+
+size_t TelemetryScalar::GetScalarSizesOfIncludingThis(
+ mozilla::MallocSizeOf aMallocSizeOf) {
+ StaticMutexAutoLock locker(gTelemetryScalarsMutex);
+ size_t n = 0;
+
+ auto getSizeOf = [aMallocSizeOf](auto& storageMap) {
+ size_t partial = 0;
+ for (const auto& scalarStorage : storageMap.Values()) {
+ for (const auto& scalar : scalarStorage->Values()) {
+ partial += scalar->SizeOfIncludingThis(aMallocSizeOf);
+ }
+ }
+ return partial;
+ };
+
+ // Account for all the storage used for the different scalar types.
+ n += getSizeOf(gScalarStorageMap);
+ n += getSizeOf(gKeyedScalarStorageMap);
+ n += getSizeOf(gDynamicBuiltinScalarStorageMap);
+ n += getSizeOf(gDynamicBuiltinKeyedScalarStorageMap);
+
+ return n;
+}
+
+void TelemetryScalar::UpdateChildData(
+ ProcessID aProcessType,
+ const nsTArray<mozilla::Telemetry::ScalarAction>& aScalarActions) {
+ MOZ_ASSERT(XRE_IsParentProcess(),
+ "The stored child processes scalar data must be updated from the "
+ "parent process.");
+ StaticMutexAutoLock locker(gTelemetryScalarsMutex);
+
+ // If scalars are still being deserialized, we need to record the incoming
+ // operations as well.
+ if (internal_IsScalarDeserializing(locker)) {
+ for (const ScalarAction& action : aScalarActions) {
+ // We're only getting immutable access, so let's copy it
+ ScalarAction copy = action;
+ // Fix up the process type
+ copy.mProcessType = aProcessType;
+ internal_RecordScalarAction(locker, copy);
+ }
+
+ return;
+ }
+
+ internal_ApplyScalarActions(locker, aScalarActions, Some(aProcessType));
+}
+
+void TelemetryScalar::UpdateChildKeyedData(
+ ProcessID aProcessType,
+ const nsTArray<mozilla::Telemetry::KeyedScalarAction>& aScalarActions) {
+ MOZ_ASSERT(XRE_IsParentProcess(),
+ "The stored child processes keyed scalar data must be updated "
+ "from the parent process.");
+ StaticMutexAutoLock locker(gTelemetryScalarsMutex);
+
+ // If scalars are still being deserialized, we need to record the incoming
+ // operations as well.
+ if (internal_IsScalarDeserializing(locker)) {
+ for (const KeyedScalarAction& action : aScalarActions) {
+ // We're only getting immutable access, so let's copy it
+ KeyedScalarAction copy = action;
+ // Fix up the process type
+ copy.mProcessType = aProcessType;
+ internal_RecordKeyedScalarAction(locker, copy);
+ }
+
+ return;
+ }
+
+ internal_ApplyKeyedScalarActions(locker, aScalarActions, Some(aProcessType));
+}
+
+void TelemetryScalar::RecordDiscardedData(
+ ProcessID aProcessType,
+ const mozilla::Telemetry::DiscardedData& aDiscardedData) {
+ MOZ_ASSERT(XRE_IsParentProcess(),
+ "Discarded Data must be updated from the parent process.");
+ StaticMutexAutoLock locker(gTelemetryScalarsMutex);
+ if (!internal_CanRecordBase(locker)) {
+ return;
+ }
+
+ if (GetCurrentProduct() == SupportedProduct::GeckoviewStreaming) {
+ return;
+ }
+
+ ScalarBase* scalar = nullptr;
+ mozilla::DebugOnly<nsresult> rv;
+
+ rv = internal_GetScalarByEnum(
+ locker,
+ ScalarKey{
+ static_cast<uint32_t>(ScalarID::TELEMETRY_DISCARDED_ACCUMULATIONS),
+ false},
+ aProcessType, &scalar);
+ MOZ_ASSERT(NS_SUCCEEDED(rv));
+ scalar->AddValue(aDiscardedData.mDiscardedHistogramAccumulations);
+
+ rv = internal_GetScalarByEnum(
+ locker,
+ ScalarKey{static_cast<uint32_t>(
+ ScalarID::TELEMETRY_DISCARDED_KEYED_ACCUMULATIONS),
+ false},
+ aProcessType, &scalar);
+ MOZ_ASSERT(NS_SUCCEEDED(rv));
+ scalar->AddValue(aDiscardedData.mDiscardedKeyedHistogramAccumulations);
+
+ rv = internal_GetScalarByEnum(
+ locker,
+ ScalarKey{
+ static_cast<uint32_t>(ScalarID::TELEMETRY_DISCARDED_SCALAR_ACTIONS),
+ false},
+ aProcessType, &scalar);
+ MOZ_ASSERT(NS_SUCCEEDED(rv));
+ scalar->AddValue(aDiscardedData.mDiscardedScalarActions);
+
+ rv = internal_GetScalarByEnum(
+ locker,
+ ScalarKey{static_cast<uint32_t>(
+ ScalarID::TELEMETRY_DISCARDED_KEYED_SCALAR_ACTIONS),
+ false},
+ aProcessType, &scalar);
+ MOZ_ASSERT(NS_SUCCEEDED(rv));
+ scalar->AddValue(aDiscardedData.mDiscardedKeyedScalarActions);
+
+ rv = internal_GetScalarByEnum(
+ locker,
+ ScalarKey{
+ static_cast<uint32_t>(ScalarID::TELEMETRY_DISCARDED_CHILD_EVENTS),
+ false},
+ aProcessType, &scalar);
+ MOZ_ASSERT(NS_SUCCEEDED(rv));
+ scalar->AddValue(aDiscardedData.mDiscardedChildEvents);
+}
+
+/**
+ * Get the dynamic scalar definitions in an IPC-friendly
+ * structure.
+ */
+void TelemetryScalar::GetDynamicScalarDefinitions(
+ nsTArray<DynamicScalarDefinition>& aDefArray) {
+ MOZ_ASSERT(XRE_IsParentProcess());
+ if (!gDynamicScalarInfo) {
+ // Don't have dynamic scalar definitions. Bail out!
+ return;
+ }
+
+ StaticMutexAutoLock locker(gTelemetryScalarsMutex);
+ internal_DynamicScalarToIPC(locker, *gDynamicScalarInfo, aDefArray);
+}
+
+/**
+ * This adds the dynamic scalar definitions coming from
+ * the parent process to this child process. If a dynamic
+ * scalar definition is already defined, check if the new definition
+ * makes the scalar expired and eventually update the expiration
+ * state.
+ */
+void TelemetryScalar::AddDynamicScalarDefinitions(
+ const nsTArray<DynamicScalarDefinition>& aDefs) {
+ MOZ_ASSERT(!XRE_IsParentProcess());
+
+ nsTArray<DynamicScalarInfo> dynamicStubs;
+
+ // Populate the definitions array before acquiring the lock.
+ for (auto& def : aDefs) {
+ bool recordOnRelease = def.dataset == nsITelemetry::DATASET_ALL_CHANNELS;
+ dynamicStubs.AppendElement(DynamicScalarInfo{def.type,
+ recordOnRelease,
+ def.expired,
+ def.name,
+ def.keyed,
+ def.builtin,
+ {} /* stores */});
+ }
+
+ {
+ StaticMutexAutoLock locker(gTelemetryScalarsMutex);
+ internal_RegisterScalars(locker, dynamicStubs);
+ }
+}
+
+nsresult TelemetryScalar::GetAllStores(StringHashSet& set) {
+ // Static stores
+ for (uint32_t storeIdx : gScalarStoresTable) {
+ const char* name = &gScalarsStringTable[storeIdx];
+ nsAutoCString store;
+ store.AssignASCII(name);
+ if (!set.Insert(store, mozilla::fallible)) {
+ return NS_ERROR_FAILURE;
+ }
+ }
+
+ // Dynamic stores
+ for (auto& ptr : *gDynamicStoreNames) {
+ nsAutoCString store;
+ ptr->ToUTF8String(store);
+ if (!set.Insert(store, mozilla::fallible)) {
+ return NS_ERROR_FAILURE;
+ }
+ }
+
+ return NS_OK;
+}
+
+////////////////////////////////////////////////////////////////////////
+////////////////////////////////////////////////////////////////////////
+//
+// PUBLIC: GeckoView serialization/deserialization functions.
+
+/**
+ * Write the scalar data to the provided Json object, for
+ * GeckoView measurement persistence. The output format is the same one used
+ * for snapshotting the scalars.
+ *
+ * @param {aWriter} The JSON object to write to.
+ * @returns NS_OK or a failure value explaining why persistence failed.
+ */
+nsresult TelemetryScalar::SerializeScalars(mozilla::JSONWriter& aWriter) {
+ // Get a copy of the data, without clearing.
+ ScalarSnapshotTable scalarsToReflect;
+ {
+ StaticMutexAutoLock locker(gTelemetryScalarsMutex);
+ // For persistence, we care about all the datasets. Worst case, they
+ // will be empty.
+ nsresult rv = internal_GetScalarSnapshot(
+ locker, scalarsToReflect, nsITelemetry::DATASET_PRERELEASE_CHANNELS,
+ false, /*aClearScalars*/
+ "main"_ns);
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+ }
+
+ // Persist the scalars to the JSON object.
+ for (const auto& entry : scalarsToReflect) {
+ const ScalarTupleArray& processScalars = entry.GetData();
+ const char* processName = GetNameForProcessID(ProcessID(entry.GetKey()));
+
+ aWriter.StartObjectProperty(mozilla::MakeStringSpan(processName));
+
+ for (const ScalarDataTuple& scalar : processScalars) {
+ nsresult rv = WriteVariantToJSONWriter(
+ std::get<2>(scalar) /*aScalarType*/,
+ std::get<1>(scalar) /*aInputValue*/,
+ mozilla::MakeStringSpan(std::get<0>(scalar)) /*aPropertyName*/,
+ aWriter /*aWriter*/);
+ if (NS_FAILED(rv)) {
+ // Skip this scalar if we failed to write it. We don't bail out just
+ // yet as we may salvage other scalars. We eventually need to call
+ // EndObject.
+ continue;
+ }
+ }
+
+ aWriter.EndObject();
+ }
+
+ return NS_OK;
+}
+
+/**
+ * Write the keyed scalar data to the provided Json object, for
+ * GeckoView measurement persistence. The output format is the same
+ * one used for snapshotting the keyed scalars.
+ *
+ * @param {aWriter} The JSON object to write to.
+ * @returns NS_OK or a failure value explaining why persistence failed.
+ */
+nsresult TelemetryScalar::SerializeKeyedScalars(mozilla::JSONWriter& aWriter) {
+ // Get a copy of the data, without clearing.
+ KeyedScalarSnapshotTable keyedScalarsToReflect;
+ {
+ StaticMutexAutoLock locker(gTelemetryScalarsMutex);
+ // For persistence, we care about all the datasets. Worst case, they
+ // will be empty.
+ nsresult rv = internal_GetKeyedScalarSnapshot(
+ locker, keyedScalarsToReflect,
+ nsITelemetry::DATASET_PRERELEASE_CHANNELS, false, /*aClearScalars*/
+ "main"_ns);
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+ }
+
+ // Persist the scalars to the JSON object.
+ for (const auto& entry : keyedScalarsToReflect) {
+ const KeyedScalarTupleArray& processScalars = entry.GetData();
+ const char* processName = GetNameForProcessID(ProcessID(entry.GetKey()));
+
+ aWriter.StartObjectProperty(mozilla::MakeStringSpan(processName));
+
+ for (const KeyedScalarDataTuple& keyedScalarData : processScalars) {
+ aWriter.StartObjectProperty(
+ mozilla::MakeStringSpan(std::get<0>(keyedScalarData)));
+
+ // Define a property for each scalar key, then add it to the keyed scalar
+ // object.
+ const nsTArray<KeyedScalar::KeyValuePair>& keyProps =
+ std::get<1>(keyedScalarData);
+ for (const KeyedScalar::KeyValuePair& keyData : keyProps) {
+ nsresult rv = WriteVariantToJSONWriter(
+ std::get<2>(keyedScalarData) /*aScalarType*/,
+ keyData.second /*aInputValue*/,
+ PromiseFlatCString(keyData.first) /*aOutKey*/, aWriter /*aWriter*/);
+ if (NS_FAILED(rv)) {
+ // Skip this scalar if we failed to write it. We don't bail out just
+ // yet as we may salvage other scalars. We eventually need to call
+ // EndObject.
+ continue;
+ }
+ }
+ aWriter.EndObject();
+ }
+ aWriter.EndObject();
+ }
+
+ return NS_OK;
+}
+
+/**
+ * Load the persisted measurements from a Json object and inject them
+ * in the relevant process storage.
+ *
+ * @param {aData} The input Json object.
+ * @returns NS_OK if loading was performed, an error code explaining the
+ * failure reason otherwise.
+ */
+nsresult TelemetryScalar::DeserializePersistedScalars(
+ JSContext* aCx, JS::Handle<JS::Value> aData) {
+ MOZ_ASSERT(XRE_IsParentProcess(), "Only load scalars in the parent process");
+ if (!XRE_IsParentProcess()) {
+ return NS_ERROR_FAILURE;
+ }
+
+ typedef std::pair<nsCString, nsCOMPtr<nsIVariant>> PersistedScalarPair;
+ typedef nsTArray<PersistedScalarPair> PersistedScalarArray;
+ typedef nsTHashMap<ProcessIDHashKey, PersistedScalarArray>
+ PeristedScalarStorage;
+
+ PeristedScalarStorage scalarsToUpdate;
+
+ // Before updating the scalars, we need to get the data out of the JS
+ // wrappers. We can't hold the scalars mutex while handling JS stuff.
+ // Build a <scalar name, value> map.
+ JS::Rooted<JSObject*> scalarDataObj(aCx, &aData.toObject());
+ JS::Rooted<JS::IdVector> processes(aCx, JS::IdVector(aCx));
+ if (!JS_Enumerate(aCx, scalarDataObj, &processes)) {
+ // We can't even enumerate the processes in the loaded data, so
+ // there is nothing we could recover from the persistence file. Bail out.
+ JS_ClearPendingException(aCx);
+ return NS_ERROR_FAILURE;
+ }
+
+ // The following block of code attempts to extract as much data as possible
+ // from the serialized JSON, even in case of light data corruptions: if, for
+ // example, the data for a single process is corrupted or is in an unexpected
+ // form, we press on and attempt to load the data for the other processes.
+ JS::Rooted<JS::PropertyKey> process(aCx);
+ for (auto& processVal : processes) {
+ // This is required as JS API calls require an Handle<jsid> and not a
+ // plain jsid.
+ process = processVal;
+ // Get the process name.
+ nsAutoJSString processNameJS;
+ if (!processNameJS.init(aCx, process)) {
+ JS_ClearPendingException(aCx);
+ continue;
+ }
+
+ // Make sure it's valid. Note that this is safe to call outside
+ // of a locked section.
+ NS_ConvertUTF16toUTF8 processName(processNameJS);
+ ProcessID processID = GetIDForProcessName(processName.get());
+ if (processID == ProcessID::Count) {
+ NS_WARNING(
+ nsPrintfCString("Failed to get process ID for %s", processName.get())
+ .get());
+ continue;
+ }
+
+ // And its probes.
+ JS::Rooted<JS::Value> processData(aCx);
+ if (!JS_GetPropertyById(aCx, scalarDataObj, process, &processData)) {
+ JS_ClearPendingException(aCx);
+ continue;
+ }
+
+ if (!processData.isObject()) {
+ // |processData| should be an object containing scalars. If this is
+ // not the case, silently skip and try to load the data for the other
+ // processes.
+ continue;
+ }
+
+ // Iterate through each scalar.
+ JS::Rooted<JSObject*> processDataObj(aCx, &processData.toObject());
+ JS::Rooted<JS::IdVector> scalars(aCx, JS::IdVector(aCx));
+ if (!JS_Enumerate(aCx, processDataObj, &scalars)) {
+ JS_ClearPendingException(aCx);
+ continue;
+ }
+
+ JS::Rooted<JS::PropertyKey> scalar(aCx);
+ for (auto& scalarVal : scalars) {
+ scalar = scalarVal;
+ // Get the scalar name.
+ nsAutoJSString scalarName;
+ if (!scalarName.init(aCx, scalar)) {
+ JS_ClearPendingException(aCx);
+ continue;
+ }
+
+ // Get the scalar value as a JS value.
+ JS::Rooted<JS::Value> scalarValue(aCx);
+ if (!JS_GetPropertyById(aCx, processDataObj, scalar, &scalarValue)) {
+ JS_ClearPendingException(aCx);
+ continue;
+ }
+
+ if (scalarValue.isNullOrUndefined()) {
+ // We can't set scalars to null or undefined values, skip this
+ // and try to load other scalars.
+ continue;
+ }
+
+ // Unpack the aVal to nsIVariant.
+ nsCOMPtr<nsIVariant> unpackedVal;
+ nsresult rv = nsContentUtils::XPConnect()->JSToVariant(
+ aCx, scalarValue, getter_AddRefs(unpackedVal));
+ if (NS_FAILED(rv)) {
+ JS_ClearPendingException(aCx);
+ continue;
+ }
+
+ // Add the scalar to the map.
+ PersistedScalarArray& processScalars =
+ scalarsToUpdate.LookupOrInsert(static_cast<uint32_t>(processID));
+ processScalars.AppendElement(std::make_pair(
+ nsCString(NS_ConvertUTF16toUTF8(scalarName)), unpackedVal));
+ }
+ }
+
+ // Now that all the JS specific operations are finished, update the scalars.
+ {
+ StaticMutexAutoLock lock(gTelemetryScalarsMutex);
+
+ for (const auto& entry : scalarsToUpdate) {
+ const PersistedScalarArray& processScalars = entry.GetData();
+ for (PersistedScalarArray::size_type i = 0; i < processScalars.Length();
+ i++) {
+ mozilla::Unused << internal_UpdateScalar(
+ lock, processScalars[i].first, ScalarActionType::eSet,
+ processScalars[i].second, ProcessID(entry.GetKey()),
+ true /* aForce */);
+ }
+ }
+ }
+
+ return NS_OK;
+}
+
+/**
+ * Load the persisted measurements from a Json object and injects them
+ * in the relevant process storage.
+ *
+ * @param {aData} The input Json object.
+ * @returns NS_OK if loading was performed, an error code explaining the
+ * failure reason otherwise.
+ */
+nsresult TelemetryScalar::DeserializePersistedKeyedScalars(
+ JSContext* aCx, JS::Handle<JS::Value> aData) {
+ MOZ_ASSERT(XRE_IsParentProcess(), "Only load scalars in the parent process");
+ if (!XRE_IsParentProcess()) {
+ return NS_ERROR_FAILURE;
+ }
+
+ typedef std::tuple<nsCString, nsString, nsCOMPtr<nsIVariant>>
+ PersistedKeyedScalarTuple;
+ typedef nsTArray<PersistedKeyedScalarTuple> PersistedKeyedScalarArray;
+ typedef nsTHashMap<ProcessIDHashKey, PersistedKeyedScalarArray>
+ PeristedKeyedScalarStorage;
+
+ PeristedKeyedScalarStorage scalarsToUpdate;
+
+ // Before updating the keyed scalars, we need to get the data out of the JS
+ // wrappers. We can't hold the scalars mutex while handling JS stuff.
+ // Build a <scalar name, value> map.
+ JS::Rooted<JSObject*> scalarDataObj(aCx, &aData.toObject());
+ JS::Rooted<JS::IdVector> processes(aCx, JS::IdVector(aCx));
+ if (!JS_Enumerate(aCx, scalarDataObj, &processes)) {
+ // We can't even enumerate the processes in the loaded data, so
+ // there is nothing we could recover from the persistence file. Bail out.
+ JS_ClearPendingException(aCx);
+ return NS_ERROR_FAILURE;
+ }
+
+ // The following block of code attempts to extract as much data as possible
+ // from the serialized JSON, even in case of light data corruptions: if, for
+ // example, the data for a single process is corrupted or is in an unexpected
+ // form, we press on and attempt to load the data for the other processes.
+ JS::Rooted<JS::PropertyKey> process(aCx);
+ for (auto& processVal : processes) {
+ process = processVal;
+ // Get the process name.
+ nsAutoJSString processNameJS;
+ if (!processNameJS.init(aCx, process)) {
+ JS_ClearPendingException(aCx);
+ continue;
+ }
+
+ // Make sure it's valid. Note that this is safe to call outside
+ // of a locked section.
+ NS_ConvertUTF16toUTF8 processName(processNameJS);
+ ProcessID processID = GetIDForProcessName(processName.get());
+ if (processID == ProcessID::Count) {
+ NS_WARNING(
+ nsPrintfCString("Failed to get process ID for %s", processName.get())
+ .get());
+ continue;
+ }
+
+ // And its probes.
+ JS::Rooted<JS::Value> processData(aCx);
+ if (!JS_GetPropertyById(aCx, scalarDataObj, process, &processData)) {
+ JS_ClearPendingException(aCx);
+ continue;
+ }
+
+ if (!processData.isObject()) {
+ // |processData| should be an object containing scalars. If this is
+ // not the case, silently skip and try to load the data for the other
+ // processes.
+ continue;
+ }
+
+ // Iterate through each keyed scalar.
+ JS::Rooted<JSObject*> processDataObj(aCx, &processData.toObject());
+ JS::Rooted<JS::IdVector> keyedScalars(aCx, JS::IdVector(aCx));
+ if (!JS_Enumerate(aCx, processDataObj, &keyedScalars)) {
+ JS_ClearPendingException(aCx);
+ continue;
+ }
+
+ JS::Rooted<JS::PropertyKey> keyedScalar(aCx);
+ for (auto& keyedScalarVal : keyedScalars) {
+ keyedScalar = keyedScalarVal;
+ // Get the scalar name.
+ nsAutoJSString scalarName;
+ if (!scalarName.init(aCx, keyedScalar)) {
+ JS_ClearPendingException(aCx);
+ continue;
+ }
+
+ // Get the data for this keyed scalar.
+ JS::Rooted<JS::Value> keyedScalarData(aCx);
+ if (!JS_GetPropertyById(aCx, processDataObj, keyedScalar,
+ &keyedScalarData)) {
+ JS_ClearPendingException(aCx);
+ continue;
+ }
+
+ if (!keyedScalarData.isObject()) {
+ // Keyed scalar data need to be an object. If that's not the case, skip
+ // it and try to load the rest of the data.
+ continue;
+ }
+
+ // Get the keys in the keyed scalar.
+ JS::Rooted<JSObject*> keyedScalarDataObj(aCx,
+ &keyedScalarData.toObject());
+ JS::Rooted<JS::IdVector> keys(aCx, JS::IdVector(aCx));
+ if (!JS_Enumerate(aCx, keyedScalarDataObj, &keys)) {
+ JS_ClearPendingException(aCx);
+ continue;
+ }
+
+ JS::Rooted<JS::PropertyKey> key(aCx);
+ for (auto keyVal : keys) {
+ key = keyVal;
+ // Get the process name.
+ nsAutoJSString keyName;
+ if (!keyName.init(aCx, key)) {
+ JS_ClearPendingException(aCx);
+ continue;
+ }
+
+ // Get the scalar value as a JS value.
+ JS::Rooted<JS::Value> scalarValue(aCx);
+ if (!JS_GetPropertyById(aCx, keyedScalarDataObj, key, &scalarValue)) {
+ JS_ClearPendingException(aCx);
+ continue;
+ }
+
+ if (scalarValue.isNullOrUndefined()) {
+ // We can't set scalars to null or undefined values, skip this
+ // and try to load other scalars.
+ continue;
+ }
+
+ // Unpack the aVal to nsIVariant.
+ nsCOMPtr<nsIVariant> unpackedVal;
+ nsresult rv = nsContentUtils::XPConnect()->JSToVariant(
+ aCx, scalarValue, getter_AddRefs(unpackedVal));
+ if (NS_FAILED(rv)) {
+ JS_ClearPendingException(aCx);
+ continue;
+ }
+
+ // Add the scalar to the map.
+ PersistedKeyedScalarArray& processScalars =
+ scalarsToUpdate.LookupOrInsert(static_cast<uint32_t>(processID));
+ processScalars.AppendElement(
+ std::make_tuple(nsCString(NS_ConvertUTF16toUTF8(scalarName)),
+ nsString(keyName), unpackedVal));
+ }
+ }
+ }
+
+ // Now that all the JS specific operations are finished, update the scalars.
+ {
+ StaticMutexAutoLock lock(gTelemetryScalarsMutex);
+
+ for (const auto& entry : scalarsToUpdate) {
+ const PersistedKeyedScalarArray& processScalars = entry.GetData();
+ for (PersistedKeyedScalarArray::size_type i = 0;
+ i < processScalars.Length(); i++) {
+ mozilla::Unused << internal_UpdateKeyedScalar(
+ lock, std::get<0>(processScalars[i]),
+ std::get<1>(processScalars[i]), ScalarActionType::eSet,
+ std::get<2>(processScalars[i]), ProcessID(entry.GetKey()),
+ true /* aForce */);
+ }
+ }
+ }
+
+ return NS_OK;
+}
diff --git a/toolkit/components/telemetry/core/TelemetryScalar.h b/toolkit/components/telemetry/core/TelemetryScalar.h
new file mode 100644
index 0000000000..c7e5352860
--- /dev/null
+++ b/toolkit/components/telemetry/core/TelemetryScalar.h
@@ -0,0 +1,133 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2; -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef TelemetryScalar_h__
+#define TelemetryScalar_h__
+
+#include <stdint.h>
+#include "mozilla/TelemetryProcessEnums.h"
+#include "mozilla/TelemetryScalarEnums.h"
+#include "nsTArray.h"
+#include "TelemetryCommon.h"
+
+// This module is internal to Telemetry. It encapsulates Telemetry's
+// scalar accumulation and storage logic. It should only be used by
+// Telemetry.cpp. These functions should not be used anywhere else.
+// For the public interface to Telemetry functionality, see Telemetry.h.
+
+namespace mozilla {
+// This is only used for the GeckoView persistence.
+class JSONWriter;
+namespace Telemetry {
+struct ScalarAction;
+struct KeyedScalarAction;
+struct DiscardedData;
+struct DynamicScalarDefinition;
+} // namespace Telemetry
+} // namespace mozilla
+
+namespace TelemetryScalar {
+
+void InitializeGlobalState(bool canRecordBase, bool canRecordExtended);
+void DeInitializeGlobalState();
+
+void SetCanRecordBase(bool b);
+void SetCanRecordExtended(bool b);
+
+// JS API Endpoints.
+nsresult Add(const nsACString& aName, JS::Handle<JS::Value> aVal,
+ JSContext* aCx);
+nsresult Set(const nsACString& aName, JS::Handle<JS::Value> aVal,
+ JSContext* aCx);
+nsresult SetMaximum(const nsACString& aName, JS::Handle<JS::Value> aVal,
+ JSContext* aCx);
+nsresult CreateSnapshots(unsigned int aDataset, bool aClearScalars,
+ JSContext* aCx, uint8_t optional_argc,
+ JS::MutableHandle<JS::Value> aResult, bool aFilterTest,
+ const nsACString& aStoreName);
+
+// Keyed JS API Endpoints.
+nsresult Add(const nsACString& aName, const nsAString& aKey,
+ JS::Handle<JS::Value> aVal, JSContext* aCx);
+nsresult Set(const nsACString& aName, const nsAString& aKey,
+ JS::Handle<JS::Value> aVal, JSContext* aCx);
+nsresult SetMaximum(const nsACString& aName, const nsAString& aKey,
+ JS::Handle<JS::Value> aVal, JSContext* aCx);
+nsresult CreateKeyedSnapshots(unsigned int aDataset, bool aClearScalars,
+ JSContext* aCx, uint8_t optional_argc,
+ JS::MutableHandle<JS::Value> aResult,
+ bool aFilterTest, const nsACString& aStoreName);
+
+// C++ API Endpoints.
+void Add(mozilla::Telemetry::ScalarID aId, uint32_t aValue);
+void Set(mozilla::Telemetry::ScalarID aId, uint32_t aValue);
+void Set(mozilla::Telemetry::ScalarID aId, const nsAString& aValue);
+void Set(mozilla::Telemetry::ScalarID aId, bool aValue);
+void SetMaximum(mozilla::Telemetry::ScalarID aId, uint32_t aValue);
+
+// Keyed C++ API Endpoints.
+void Add(mozilla::Telemetry::ScalarID aId, const nsAString& aKey,
+ uint32_t aValue);
+void Set(mozilla::Telemetry::ScalarID aId, const nsAString& aKey,
+ uint32_t aValue);
+void Set(mozilla::Telemetry::ScalarID aId, const nsAString& aKey, bool aValue);
+void SetMaximum(mozilla::Telemetry::ScalarID aId, const nsAString& aKey,
+ uint32_t aValue);
+
+nsresult RegisterScalars(const nsACString& aCategoryName,
+ JS::Handle<JS::Value> aScalarData, bool aBuiltin,
+ JSContext* cx);
+
+// Event Summary
+void SummarizeEvent(const nsCString& aUniqueEventName,
+ mozilla::Telemetry::ProcessID aProcessType, bool aDynamic);
+
+// Only to be used for testing.
+void ClearScalars();
+
+size_t GetMapShallowSizesOfExcludingThis(mozilla::MallocSizeOf aMallocSizeOf);
+size_t GetScalarSizesOfIncludingThis(mozilla::MallocSizeOf aMallocSizeOf);
+
+void UpdateChildData(
+ mozilla::Telemetry::ProcessID aProcessType,
+ const nsTArray<mozilla::Telemetry::ScalarAction>& aScalarActions);
+
+void UpdateChildKeyedData(
+ mozilla::Telemetry::ProcessID aProcessType,
+ const nsTArray<mozilla::Telemetry::KeyedScalarAction>& aScalarActions);
+
+void RecordDiscardedData(
+ mozilla::Telemetry::ProcessID aProcessType,
+ const mozilla::Telemetry::DiscardedData& aDiscardedData);
+
+void GetDynamicScalarDefinitions(
+ nsTArray<mozilla::Telemetry::DynamicScalarDefinition>&);
+void AddDynamicScalarDefinitions(
+ const nsTArray<mozilla::Telemetry::DynamicScalarDefinition>&);
+
+/**
+ * Append the list of registered stores to the given set.
+ * This includes dynamic stores.
+ */
+nsresult GetAllStores(mozilla::Telemetry::Common::StringHashSet& set);
+
+// They are responsible for updating in-memory probes with the data persisted
+// on the disk and vice-versa.
+nsresult SerializeScalars(mozilla::JSONWriter& aWriter);
+nsresult SerializeKeyedScalars(mozilla::JSONWriter& aWriter);
+nsresult DeserializePersistedScalars(JSContext* aCx,
+ JS::Handle<JS::Value> aData);
+nsresult DeserializePersistedKeyedScalars(JSContext* aCx,
+ JS::Handle<JS::Value> aData);
+// Mark deserialization as in progress.
+// After this, all scalar operations are recorded into the pending operations
+// list.
+void DeserializationStarted();
+// Apply all operations from the pending operations list and mark
+// deserialization finished afterwards.
+void ApplyPendingOperations();
+} // namespace TelemetryScalar
+
+#endif // TelemetryScalar_h__
diff --git a/toolkit/components/telemetry/core/TelemetryUserInteraction.cpp b/toolkit/components/telemetry/core/TelemetryUserInteraction.cpp
new file mode 100644
index 0000000000..ad6601c22c
--- /dev/null
+++ b/toolkit/components/telemetry/core/TelemetryUserInteraction.cpp
@@ -0,0 +1,101 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "nsString.h"
+#include "MainThreadUtils.h"
+#include "TelemetryUserInteraction.h"
+#include "TelemetryUserInteractionData.h"
+#include "TelemetryUserInteractionNameMap.h"
+#include "UserInteractionInfo.h"
+
+using mozilla::Telemetry::UserInteractionIDByNameLookup;
+
+////////////////////////////////////////////////////////////////////////
+////////////////////////////////////////////////////////////////////////
+//
+// PRIVATE TYPES
+namespace {
+
+struct UserInteractionKey {
+ uint32_t id;
+};
+
+} // namespace
+
+////////////////////////////////////////////////////////////////////////
+////////////////////////////////////////////////////////////////////////
+//
+// PRIVATE STATE, SHARED BY ALL THREADS
+
+namespace {
+// Set to true once this global state has been initialized.
+bool gInitDone = false;
+
+bool gCanRecordBase;
+bool gCanRecordExtended;
+} // namespace
+
+namespace {
+// Implements the methods for UserInteractionInfo.
+const char* UserInteractionInfo::name() const {
+ return &gUserInteractionsStringTable[this->name_offset];
+}
+
+} // namespace
+
+////////////////////////////////////////////////////////////////////////
+////////////////////////////////////////////////////////////////////////
+//
+// EXTERNALLY VISIBLE FUNCTIONS in namespace TelemetryUserInteraction::
+
+void TelemetryUserInteraction::InitializeGlobalState(bool aCanRecordBase,
+ bool aCanRecordExtended) {
+ if (!XRE_IsParentProcess()) {
+ return;
+ }
+
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(!gInitDone,
+ "TelemetryUserInteraction::InitializeGlobalState "
+ "may only be called once");
+
+ gCanRecordBase = aCanRecordBase;
+ gCanRecordExtended = aCanRecordExtended;
+ gInitDone = true;
+}
+
+void TelemetryUserInteraction::DeInitializeGlobalState() {
+ if (!XRE_IsParentProcess()) {
+ return;
+ }
+
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(gInitDone);
+ if (!gInitDone) {
+ return;
+ }
+
+ gInitDone = false;
+}
+
+bool TelemetryUserInteraction::CanRecord(const nsAString& aName) {
+ if (!gCanRecordBase) {
+ return false;
+ }
+
+ nsCString name = NS_ConvertUTF16toUTF8(aName);
+ const uint32_t idx = UserInteractionIDByNameLookup(name);
+
+ MOZ_DIAGNOSTIC_ASSERT(
+ idx < mozilla::Telemetry::UserInteractionID::UserInteractionCount,
+ "Intermediate lookup should always give a valid index.");
+
+ if (name.Equals(gUserInteractions[idx].name())) {
+ return true;
+ }
+
+ return false;
+}
diff --git a/toolkit/components/telemetry/core/TelemetryUserInteraction.h b/toolkit/components/telemetry/core/TelemetryUserInteraction.h
new file mode 100644
index 0000000000..2b12a5f275
--- /dev/null
+++ b/toolkit/components/telemetry/core/TelemetryUserInteraction.h
@@ -0,0 +1,20 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2; -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef TelemetryUserInteraction_h__
+#define TelemetryUserInteraction_h__
+
+#include "nsStringFwd.h"
+
+namespace TelemetryUserInteraction {
+
+void InitializeGlobalState(bool canRecordBase, bool canRecordExtended);
+void DeInitializeGlobalState();
+
+bool CanRecord(const nsAString& aName);
+
+} // namespace TelemetryUserInteraction
+
+#endif // TelemetryUserInteraction_h__
diff --git a/toolkit/components/telemetry/core/UserInteractionInfo.h b/toolkit/components/telemetry/core/UserInteractionInfo.h
new file mode 100644
index 0000000000..e20017b790
--- /dev/null
+++ b/toolkit/components/telemetry/core/UserInteractionInfo.h
@@ -0,0 +1,30 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2; -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef TelemetryUserInteractionInfo_h__
+#define TelemetryUserInteractionInfo_h__
+
+#include "TelemetryCommon.h"
+
+// This module is internal to Telemetry. It defines a structure that holds the
+// UserInteraction info. It should only be used by
+// TelemetryUserInteractionData.h automatically generated file and
+// TelemetryUserInteraction.cpp. This should not be used anywhere else. For the
+// public interface to Telemetry functionality, see Telemetry.h.
+
+namespace {
+
+struct UserInteractionInfo {
+ const uint32_t name_offset;
+
+ explicit constexpr UserInteractionInfo(const uint32_t aNameOffset)
+ : name_offset(aNameOffset) {}
+
+ const char* name() const;
+};
+
+} // namespace
+
+#endif // TelemetryUserInteractionInfo_h__
diff --git a/toolkit/components/telemetry/core/components.conf b/toolkit/components/telemetry/core/components.conf
new file mode 100644
index 0000000000..d003b414a1
--- /dev/null
+++ b/toolkit/components/telemetry/core/components.conf
@@ -0,0 +1,21 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+Headers = ['mozilla/Telemetry.h']
+
+UnloadFunc = 'mozilla::Telemetry::ShutdownTelemetry'
+
+Classes = [
+ {
+ 'js_name': 'telemetry',
+ 'cid': '{aea477f2-b3a2-469c-aa29-0a82d132b829}',
+ 'contract_ids': ['@mozilla.org/base/telemetry;1'],
+ 'interfaces': ['nsITelemetry'],
+ 'singleton': True,
+ 'type': 'nsITelemetry',
+ 'processes': ProcessSelector.ALLOW_IN_GPU_RDD_VR_SOCKET_UTILITY_AND_GMPLUGIN_PROCESS,
+ },
+]
diff --git a/toolkit/components/telemetry/core/ipc/TelemetryComms.h b/toolkit/components/telemetry/core/ipc/TelemetryComms.h
new file mode 100644
index 0000000000..75f59209b0
--- /dev/null
+++ b/toolkit/components/telemetry/core/ipc/TelemetryComms.h
@@ -0,0 +1,400 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2; -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef Telemetry_Comms_h__
+#define Telemetry_Comms_h__
+
+#include "ipc/IPCMessageUtils.h"
+#include "ipc/IPCMessageUtilsSpecializations.h"
+#include "mozilla/Telemetry.h"
+#include "mozilla/TelemetryProcessEnums.h"
+#include "mozilla/TimeStamp.h"
+#include "mozilla/Variant.h"
+#include "nsITelemetry.h"
+
+namespace mozilla {
+namespace Telemetry {
+
+// Histogram accumulation types.
+enum HistogramID : uint32_t;
+
+struct HistogramAccumulation {
+ mozilla::Telemetry::HistogramID mId;
+ uint32_t mSample;
+};
+
+struct KeyedHistogramAccumulation {
+ mozilla::Telemetry::HistogramID mId;
+ uint32_t mSample;
+ nsCString mKey;
+};
+
+// Scalar accumulation types.
+enum class ScalarID : uint32_t;
+
+enum class ScalarActionType : uint32_t { eSet = 0, eAdd = 1, eSetMaximum = 2 };
+
+typedef mozilla::Variant<uint32_t, bool, nsString> ScalarVariant;
+
+struct ScalarAction {
+ uint32_t mId;
+ bool mDynamic;
+ ScalarActionType mActionType;
+ // We need to wrap mData in a Maybe otherwise the IPC system
+ // is unable to instantiate a ScalarAction.
+ Maybe<ScalarVariant> mData;
+ // The process type this scalar should be recorded for.
+ // The IPC system will determine the process this action was coming from
+ // later.
+ mozilla::Telemetry::ProcessID mProcessType;
+};
+
+struct KeyedScalarAction {
+ uint32_t mId;
+ bool mDynamic;
+ ScalarActionType mActionType;
+ nsCString mKey;
+ // We need to wrap mData in a Maybe otherwise the IPC system
+ // is unable to instantiate a ScalarAction.
+ Maybe<ScalarVariant> mData;
+ // The process type this scalar should be recorded for.
+ // The IPC system will determine the process this action was coming from
+ // later.
+ mozilla::Telemetry::ProcessID mProcessType;
+};
+
+// Dynamic scalars support.
+struct DynamicScalarDefinition {
+ uint32_t type;
+ uint32_t dataset;
+ bool expired;
+ bool keyed;
+ bool builtin;
+ nsCString name;
+
+ bool operator==(const DynamicScalarDefinition& rhs) const {
+ return type == rhs.type && dataset == rhs.dataset &&
+ expired == rhs.expired && keyed == rhs.keyed &&
+ builtin == rhs.builtin && name.Equals(rhs.name);
+ }
+};
+
+struct ChildEventData {
+ mozilla::TimeStamp timestamp;
+ nsCString category;
+ nsCString method;
+ nsCString object;
+ mozilla::Maybe<nsCString> value;
+ CopyableTArray<EventExtraEntry> extra;
+};
+
+struct DiscardedData {
+ uint32_t mDiscardedHistogramAccumulations;
+ uint32_t mDiscardedKeyedHistogramAccumulations;
+ uint32_t mDiscardedScalarActions;
+ uint32_t mDiscardedKeyedScalarActions;
+ uint32_t mDiscardedChildEvents;
+};
+
+} // namespace Telemetry
+} // namespace mozilla
+
+namespace IPC {
+
+template <>
+struct ParamTraits<mozilla::Telemetry::HistogramAccumulation> {
+ typedef mozilla::Telemetry::HistogramAccumulation paramType;
+
+ static void Write(MessageWriter* aWriter, const paramType& aParam) {
+ aWriter->WriteUInt32(aParam.mId);
+ WriteParam(aWriter, aParam.mSample);
+ }
+
+ static bool Read(MessageReader* aReader, paramType* aResult) {
+ if (!aReader->ReadUInt32(reinterpret_cast<uint32_t*>(&(aResult->mId))) ||
+ !ReadParam(aReader, &(aResult->mSample))) {
+ return false;
+ }
+
+ return true;
+ }
+};
+
+template <>
+struct ParamTraits<mozilla::Telemetry::KeyedHistogramAccumulation> {
+ typedef mozilla::Telemetry::KeyedHistogramAccumulation paramType;
+
+ static void Write(MessageWriter* aWriter, const paramType& aParam) {
+ aWriter->WriteUInt32(aParam.mId);
+ WriteParam(aWriter, aParam.mSample);
+ WriteParam(aWriter, aParam.mKey);
+ }
+
+ static bool Read(MessageReader* aReader, paramType* aResult) {
+ if (!aReader->ReadUInt32(reinterpret_cast<uint32_t*>(&(aResult->mId))) ||
+ !ReadParam(aReader, &(aResult->mSample)) ||
+ !ReadParam(aReader, &(aResult->mKey))) {
+ return false;
+ }
+
+ return true;
+ }
+};
+
+/**
+ * IPC scalar data message serialization and de-serialization.
+ */
+template <>
+struct ParamTraits<mozilla::Telemetry::ScalarAction> {
+ typedef mozilla::Telemetry::ScalarAction paramType;
+
+ static void Write(MessageWriter* aWriter, const paramType& aParam) {
+ // Write the message type
+ aWriter->WriteUInt32(aParam.mId);
+ WriteParam(aWriter, aParam.mDynamic);
+ WriteParam(aWriter, static_cast<uint32_t>(aParam.mActionType));
+
+ if (aParam.mData.isNothing()) {
+ MOZ_CRASH("There is no data in the ScalarAction.");
+ return;
+ }
+
+ if (aParam.mData->is<uint32_t>()) {
+ // That's a nsITelemetry::SCALAR_TYPE_COUNT.
+ WriteParam(aWriter,
+ static_cast<uint32_t>(nsITelemetry::SCALAR_TYPE_COUNT));
+ WriteParam(aWriter, aParam.mData->as<uint32_t>());
+ } else if (aParam.mData->is<nsString>()) {
+ // That's a nsITelemetry::SCALAR_TYPE_STRING.
+ WriteParam(aWriter,
+ static_cast<uint32_t>(nsITelemetry::SCALAR_TYPE_STRING));
+ WriteParam(aWriter, aParam.mData->as<nsString>());
+ } else if (aParam.mData->is<bool>()) {
+ // That's a nsITelemetry::SCALAR_TYPE_BOOLEAN.
+ WriteParam(aWriter,
+ static_cast<uint32_t>(nsITelemetry::SCALAR_TYPE_BOOLEAN));
+ WriteParam(aWriter, aParam.mData->as<bool>());
+ } else {
+ MOZ_CRASH("Unknown scalar type.");
+ }
+ }
+
+ static bool Read(MessageReader* aReader, paramType* aResult) {
+ // Read the scalar ID and the scalar type.
+ uint32_t scalarType = 0;
+ if (!aReader->ReadUInt32(reinterpret_cast<uint32_t*>(&(aResult->mId))) ||
+ !ReadParam(aReader, reinterpret_cast<bool*>(&(aResult->mDynamic))) ||
+ !ReadParam(aReader,
+ reinterpret_cast<uint32_t*>(&(aResult->mActionType))) ||
+ !ReadParam(aReader, &scalarType)) {
+ return false;
+ }
+
+ // De-serialize the data based on the scalar type.
+ switch (scalarType) {
+ case nsITelemetry::SCALAR_TYPE_COUNT: {
+ uint32_t data = 0;
+ // De-serialize the data.
+ if (!ReadParam(aReader, &data)) {
+ return false;
+ }
+ aResult->mData = mozilla::Some(mozilla::AsVariant(data));
+ break;
+ }
+ case nsITelemetry::SCALAR_TYPE_STRING: {
+ nsString data;
+ // De-serialize the data.
+ if (!ReadParam(aReader, &data)) {
+ return false;
+ }
+ aResult->mData = mozilla::Some(mozilla::AsVariant(data));
+ break;
+ }
+ case nsITelemetry::SCALAR_TYPE_BOOLEAN: {
+ bool data = false;
+ // De-serialize the data.
+ if (!ReadParam(aReader, &data)) {
+ return false;
+ }
+ aResult->mData = mozilla::Some(mozilla::AsVariant(data));
+ break;
+ }
+ default:
+ MOZ_ASSERT(false, "Unknown scalar type.");
+ return false;
+ }
+
+ return true;
+ }
+};
+
+/**
+ * IPC keyed scalar data message serialization and de-serialization.
+ */
+template <>
+struct ParamTraits<mozilla::Telemetry::KeyedScalarAction> {
+ typedef mozilla::Telemetry::KeyedScalarAction paramType;
+
+ static void Write(MessageWriter* aWriter, const paramType& aParam) {
+ // Write the message type
+ aWriter->WriteUInt32(static_cast<uint32_t>(aParam.mId));
+ WriteParam(aWriter, aParam.mDynamic);
+ WriteParam(aWriter, static_cast<uint32_t>(aParam.mActionType));
+ WriteParam(aWriter, aParam.mKey);
+
+ if (aParam.mData.isNothing()) {
+ MOZ_CRASH("There is no data in the KeyedScalarAction.");
+ return;
+ }
+
+ if (aParam.mData->is<uint32_t>()) {
+ // That's a nsITelemetry::SCALAR_TYPE_COUNT.
+ WriteParam(aWriter,
+ static_cast<uint32_t>(nsITelemetry::SCALAR_TYPE_COUNT));
+ WriteParam(aWriter, aParam.mData->as<uint32_t>());
+ } else if (aParam.mData->is<nsString>()) {
+ // That's a nsITelemetry::SCALAR_TYPE_STRING.
+ // Keyed string scalars are not supported.
+ MOZ_ASSERT(false,
+ "Keyed String Scalar unable to be write from child process. "
+ "Not supported.");
+ } else if (aParam.mData->is<bool>()) {
+ // That's a nsITelemetry::SCALAR_TYPE_BOOLEAN.
+ WriteParam(aWriter,
+ static_cast<uint32_t>(nsITelemetry::SCALAR_TYPE_BOOLEAN));
+ WriteParam(aWriter, aParam.mData->as<bool>());
+ } else {
+ MOZ_CRASH("Unknown keyed scalar type.");
+ }
+ }
+
+ static bool Read(MessageReader* aReader, paramType* aResult) {
+ // Read the scalar ID and the scalar type.
+ uint32_t scalarType = 0;
+ if (!aReader->ReadUInt32(reinterpret_cast<uint32_t*>(&(aResult->mId))) ||
+ !ReadParam(aReader, reinterpret_cast<bool*>(&(aResult->mDynamic))) ||
+ !ReadParam(aReader,
+ reinterpret_cast<uint32_t*>(&(aResult->mActionType))) ||
+ !ReadParam(aReader, &(aResult->mKey)) ||
+ !ReadParam(aReader, &scalarType)) {
+ return false;
+ }
+
+ // De-serialize the data based on the scalar type.
+ switch (scalarType) {
+ case nsITelemetry::SCALAR_TYPE_COUNT: {
+ uint32_t data = 0;
+ // De-serialize the data.
+ if (!ReadParam(aReader, &data)) {
+ return false;
+ }
+ aResult->mData = mozilla::Some(mozilla::AsVariant(data));
+ break;
+ }
+ case nsITelemetry::SCALAR_TYPE_STRING: {
+ // Keyed string scalars are not supported.
+ MOZ_ASSERT(false,
+ "Keyed String Scalar unable to be read from child process. "
+ "Not supported.");
+ return false;
+ }
+ case nsITelemetry::SCALAR_TYPE_BOOLEAN: {
+ bool data = false;
+ // De-serialize the data.
+ if (!ReadParam(aReader, &data)) {
+ return false;
+ }
+ aResult->mData = mozilla::Some(mozilla::AsVariant(data));
+ break;
+ }
+ default:
+ MOZ_ASSERT(false, "Unknown keyed scalar type.");
+ return false;
+ }
+
+ return true;
+ }
+};
+
+template <>
+struct ParamTraits<mozilla::Telemetry::DynamicScalarDefinition> {
+ typedef mozilla::Telemetry::DynamicScalarDefinition paramType;
+
+ static void Write(MessageWriter* aWriter, const paramType& aParam) {
+ nsCString name;
+ WriteParam(aWriter, aParam.type);
+ WriteParam(aWriter, aParam.dataset);
+ WriteParam(aWriter, aParam.expired);
+ WriteParam(aWriter, aParam.keyed);
+ WriteParam(aWriter, aParam.builtin);
+ WriteParam(aWriter, aParam.name);
+ }
+
+ static bool Read(MessageReader* aReader, paramType* aResult) {
+ if (!ReadParam(aReader, reinterpret_cast<uint32_t*>(&(aResult->type))) ||
+ !ReadParam(aReader, reinterpret_cast<uint32_t*>(&(aResult->dataset))) ||
+ !ReadParam(aReader, reinterpret_cast<bool*>(&(aResult->expired))) ||
+ !ReadParam(aReader, reinterpret_cast<bool*>(&(aResult->keyed))) ||
+ !ReadParam(aReader, reinterpret_cast<bool*>(&(aResult->builtin))) ||
+ !ReadParam(aReader, &(aResult->name))) {
+ return false;
+ }
+ return true;
+ }
+};
+
+template <>
+struct ParamTraits<mozilla::Telemetry::ChildEventData> {
+ typedef mozilla::Telemetry::ChildEventData paramType;
+
+ static void Write(MessageWriter* aWriter, const paramType& aParam) {
+ WriteParam(aWriter, aParam.timestamp);
+ WriteParam(aWriter, aParam.category);
+ WriteParam(aWriter, aParam.method);
+ WriteParam(aWriter, aParam.object);
+ WriteParam(aWriter, aParam.value);
+ WriteParam(aWriter, aParam.extra);
+ }
+
+ static bool Read(MessageReader* aReader, paramType* aResult) {
+ if (!ReadParam(aReader, &(aResult->timestamp)) ||
+ !ReadParam(aReader, &(aResult->category)) ||
+ !ReadParam(aReader, &(aResult->method)) ||
+ !ReadParam(aReader, &(aResult->object)) ||
+ !ReadParam(aReader, &(aResult->value)) ||
+ !ReadParam(aReader, &(aResult->extra))) {
+ return false;
+ }
+
+ return true;
+ }
+};
+
+template <>
+struct ParamTraits<mozilla::Telemetry::EventExtraEntry> {
+ typedef mozilla::Telemetry::EventExtraEntry paramType;
+
+ static void Write(MessageWriter* aWriter, const paramType& aParam) {
+ WriteParam(aWriter, aParam.key);
+ WriteParam(aWriter, aParam.value);
+ }
+
+ static bool Read(MessageReader* aReader, paramType* aResult) {
+ if (!ReadParam(aReader, &(aResult->key)) ||
+ !ReadParam(aReader, &(aResult->value))) {
+ return false;
+ }
+
+ return true;
+ }
+};
+
+template <>
+struct ParamTraits<mozilla::Telemetry::DiscardedData>
+ : public PlainOldDataSerializer<mozilla::Telemetry::DiscardedData> {};
+
+} // namespace IPC
+
+#endif // Telemetry_Comms_h__
diff --git a/toolkit/components/telemetry/core/ipc/TelemetryIPC.cpp b/toolkit/components/telemetry/core/ipc/TelemetryIPC.cpp
new file mode 100644
index 0000000000..daeaeece65
--- /dev/null
+++ b/toolkit/components/telemetry/core/ipc/TelemetryIPC.cpp
@@ -0,0 +1,59 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "TelemetryIPC.h"
+#include "../TelemetryScalar.h"
+#include "../TelemetryHistogram.h"
+#include "../TelemetryEvent.h"
+
+namespace mozilla {
+
+void TelemetryIPC::AccumulateChildHistograms(
+ Telemetry::ProcessID aProcessType,
+ const nsTArray<Telemetry::HistogramAccumulation>& aAccumulations) {
+ TelemetryHistogram::AccumulateChild(aProcessType, aAccumulations);
+}
+
+void TelemetryIPC::AccumulateChildKeyedHistograms(
+ Telemetry::ProcessID aProcessType,
+ const nsTArray<Telemetry::KeyedHistogramAccumulation>& aAccumulations) {
+ TelemetryHistogram::AccumulateChildKeyed(aProcessType, aAccumulations);
+}
+
+void TelemetryIPC::UpdateChildScalars(
+ Telemetry::ProcessID aProcessType,
+ const nsTArray<Telemetry::ScalarAction>& aScalarActions) {
+ TelemetryScalar::UpdateChildData(aProcessType, aScalarActions);
+}
+
+void TelemetryIPC::UpdateChildKeyedScalars(
+ Telemetry::ProcessID aProcessType,
+ const nsTArray<Telemetry::KeyedScalarAction>& aScalarActions) {
+ TelemetryScalar::UpdateChildKeyedData(aProcessType, aScalarActions);
+}
+
+void TelemetryIPC::GetDynamicScalarDefinitions(
+ nsTArray<mozilla::Telemetry::DynamicScalarDefinition>& aDefs) {
+ TelemetryScalar::GetDynamicScalarDefinitions(aDefs);
+}
+
+void TelemetryIPC::AddDynamicScalarDefinitions(
+ const nsTArray<mozilla::Telemetry::DynamicScalarDefinition>& aDefs) {
+ TelemetryScalar::AddDynamicScalarDefinitions(aDefs);
+}
+
+void TelemetryIPC::RecordChildEvents(
+ Telemetry::ProcessID aProcessType,
+ const nsTArray<Telemetry::ChildEventData>& aEvents) {
+ TelemetryEvent::RecordChildEvents(aProcessType, aEvents);
+}
+
+void TelemetryIPC::RecordDiscardedData(
+ Telemetry::ProcessID aProcessType,
+ const Telemetry::DiscardedData& aDiscardedData) {
+ TelemetryScalar::RecordDiscardedData(aProcessType, aDiscardedData);
+}
+} // namespace mozilla
diff --git a/toolkit/components/telemetry/core/ipc/TelemetryIPC.h b/toolkit/components/telemetry/core/ipc/TelemetryIPC.h
new file mode 100644
index 0000000000..bf45280059
--- /dev/null
+++ b/toolkit/components/telemetry/core/ipc/TelemetryIPC.h
@@ -0,0 +1,114 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2; -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef TelemetryIPC_h__
+#define TelemetryIPC_h__
+
+#include "mozilla/TelemetryProcessEnums.h"
+#include "nsTArray.h"
+
+// This module provides the interface to accumulate Telemetry from child
+// processes. Top-level actors for different child processes types
+// (ContentParent, GPUChild) will call this for messages from their respective
+// processes.
+
+namespace mozilla {
+
+namespace Telemetry {
+
+struct HistogramAccumulation;
+struct KeyedHistogramAccumulation;
+struct ScalarAction;
+struct KeyedScalarAction;
+struct DynamicScalarDefinition;
+struct ChildEventData;
+struct DiscardedData;
+
+} // namespace Telemetry
+
+namespace TelemetryIPC {
+
+/**
+ * Accumulate child process data into histograms for the given process type.
+ *
+ * @param aProcessType - the process type to accumulate the histograms for
+ * @param aAccumulations - accumulation actions to perform
+ */
+void AccumulateChildHistograms(
+ Telemetry::ProcessID aProcessType,
+ const nsTArray<Telemetry::HistogramAccumulation>& aAccumulations);
+
+/**
+ * Accumulate child process data into keyed histograms for the given process
+ * type.
+ *
+ * @param aProcessType - the process type to accumulate the keyed histograms for
+ * @param aAccumulations - accumulation actions to perform
+ */
+void AccumulateChildKeyedHistograms(
+ Telemetry::ProcessID aProcessType,
+ const nsTArray<Telemetry::KeyedHistogramAccumulation>& aAccumulations);
+
+/**
+ * Update scalars for the given process type with the data coming from child
+ * process.
+ *
+ * @param aProcessType - the process type to process the scalar actions for
+ * @param aScalarActions - actions to update the scalar data
+ */
+void UpdateChildScalars(
+ Telemetry::ProcessID aProcessType,
+ const nsTArray<Telemetry::ScalarAction>& aScalarActions);
+
+/**
+ * Update keyed scalars for the given process type with the data coming from
+ * child process.
+ *
+ * @param aProcessType - the process type to process the keyed scalar actions
+ * for
+ * @param aScalarActions - actions to update the keyed scalar data
+ */
+void UpdateChildKeyedScalars(
+ Telemetry::ProcessID aProcessType,
+ const nsTArray<Telemetry::KeyedScalarAction>& aScalarActions);
+
+/**
+ * Record events for the given process type with the data coming from child
+ * process.
+ *
+ * @param aProcessType - the process type to record the events for
+ * @param aEvents - events to record
+ */
+void RecordChildEvents(Telemetry::ProcessID aProcessType,
+ const nsTArray<Telemetry::ChildEventData>& aEvents);
+
+/**
+ * Record the counts of data the child process had to discard
+ *
+ * @param aProcessType - the process reporting the discarded data
+ * @param aDiscardedData - stats about the discarded data
+ */
+void RecordDiscardedData(Telemetry::ProcessID aProcessType,
+ const Telemetry::DiscardedData& aDiscardedData);
+
+/**
+ * Get the dynamic scalar definitions from the parent process.
+ * @param aDefs - The array that will contain the scalar definitions.
+ */
+void GetDynamicScalarDefinitions(
+ nsTArray<mozilla::Telemetry::DynamicScalarDefinition>& aDefs);
+
+/**
+ * Add the dynamic scalar definitions coming from the parent process
+ * to the current child process.
+ * @param aDefs - The array that contains the scalar definitions.
+ */
+void AddDynamicScalarDefinitions(
+ const nsTArray<mozilla::Telemetry::DynamicScalarDefinition>& aDefs);
+
+} // namespace TelemetryIPC
+} // namespace mozilla
+
+#endif // TelemetryIPC_h__
diff --git a/toolkit/components/telemetry/core/ipc/TelemetryIPCAccumulator.cpp b/toolkit/components/telemetry/core/ipc/TelemetryIPCAccumulator.cpp
new file mode 100644
index 0000000000..fd0f945daf
--- /dev/null
+++ b/toolkit/components/telemetry/core/ipc/TelemetryIPCAccumulator.cpp
@@ -0,0 +1,351 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "TelemetryIPCAccumulator.h"
+
+#include "core/TelemetryScalar.h"
+#include "mozilla/TelemetryProcessEnums.h"
+#include "mozilla/dom/ContentChild.h"
+#include "mozilla/gfx/GPUParent.h"
+#include "mozilla/RDDParent.h"
+#include "mozilla/net/SocketProcessChild.h"
+#include "mozilla/ipc/UtilityProcessChild.h"
+#include "mozilla/SchedulerGroup.h"
+#include "mozilla/StaticMutex.h"
+#include "mozilla/StaticPrefs_toolkit.h"
+#include "mozilla/StaticPtr.h"
+#include "mozilla/Unused.h"
+#include "nsITimer.h"
+#include "nsThreadUtils.h"
+
+using mozilla::StaticAutoPtr;
+using mozilla::StaticMutex;
+using mozilla::StaticMutexAutoLock;
+using mozilla::Telemetry::ChildEventData;
+using mozilla::Telemetry::DiscardedData;
+using mozilla::Telemetry::HistogramAccumulation;
+using mozilla::Telemetry::KeyedHistogramAccumulation;
+using mozilla::Telemetry::KeyedScalarAction;
+using mozilla::Telemetry::ScalarAction;
+using mozilla::Telemetry::ScalarActionType;
+using mozilla::Telemetry::ScalarVariant;
+
+namespace TelemetryIPCAccumulator = mozilla::TelemetryIPCAccumulator;
+
+// To stop growing unbounded in memory while waiting for
+// StaticPrefs::toolkit_telemetry_ipcBatchTimeout() milliseconds to drain the
+// probe accumulation arrays, we request an immediate flush if the arrays
+// manage to reach certain high water mark of elements.
+const size_t kHistogramAccumulationsArrayHighWaterMark = 5 * 1024;
+const size_t kScalarActionsArrayHighWaterMark = 10000;
+// With the current limits, events cost us about 1100 bytes each.
+// This limits memory use to about 10MB.
+const size_t kEventsArrayHighWaterMark = 10000;
+// If we are starved we can overshoot the watermark.
+// This is the multiplier over which we will discard data.
+const size_t kWaterMarkDiscardFactor = 5;
+
+// Counts of how many pieces of data we have discarded.
+DiscardedData gDiscardedData = {0};
+
+// This timer is used for batching and sending child process accumulations to
+// the parent.
+nsITimer* gIPCTimer = nullptr;
+mozilla::Atomic<bool, mozilla::Relaxed> gIPCTimerArmed(false);
+mozilla::Atomic<bool, mozilla::Relaxed> gIPCTimerArming(false);
+
+// This batches child process accumulations that should be sent to the parent.
+StaticAutoPtr<nsTArray<HistogramAccumulation>> gHistogramAccumulations;
+StaticAutoPtr<nsTArray<KeyedHistogramAccumulation>>
+ gKeyedHistogramAccumulations;
+StaticAutoPtr<nsTArray<ScalarAction>> gChildScalarsActions;
+StaticAutoPtr<nsTArray<KeyedScalarAction>> gChildKeyedScalarsActions;
+StaticAutoPtr<nsTArray<ChildEventData>> gChildEvents;
+
+// This is a StaticMutex rather than a plain Mutex so that (1)
+// it gets initialised in a thread-safe manner the first time
+// it is used, and (2) because it is never de-initialised, and
+// a normal Mutex would show up as a leak in BloatView. StaticMutex
+// also has the "OffTheBooks" property, so it won't show as a leak
+// in BloatView.
+static StaticMutex gTelemetryIPCAccumulatorMutex MOZ_UNANNOTATED;
+
+namespace {
+
+void DoArmIPCTimerMainThread(const StaticMutexAutoLock& lock) {
+ MOZ_ASSERT(NS_IsMainThread());
+ gIPCTimerArming = false;
+ if (gIPCTimerArmed) {
+ return;
+ }
+ if (!gIPCTimer) {
+ gIPCTimer = NS_NewTimer().take();
+ }
+ if (gIPCTimer) {
+ gIPCTimer->InitWithNamedFuncCallback(
+ TelemetryIPCAccumulator::IPCTimerFired, nullptr,
+ mozilla::StaticPrefs::toolkit_telemetry_ipcBatchTimeout(),
+ nsITimer::TYPE_ONE_SHOT_LOW_PRIORITY,
+ "TelemetryIPCAccumulator::IPCTimerFired");
+ gIPCTimerArmed = true;
+ }
+}
+
+void ArmIPCTimer(const StaticMutexAutoLock& lock) {
+ if (gIPCTimerArmed || gIPCTimerArming) {
+ return;
+ }
+ gIPCTimerArming = true;
+ if (NS_IsMainThread()) {
+ DoArmIPCTimerMainThread(lock);
+ } else {
+ TelemetryIPCAccumulator::DispatchToMainThread(NS_NewRunnableFunction(
+ "TelemetryIPCAccumulator::ArmIPCTimer", []() -> void {
+ StaticMutexAutoLock locker(gTelemetryIPCAccumulatorMutex);
+ DoArmIPCTimerMainThread(locker);
+ }));
+ }
+}
+
+void DispatchIPCTimerFired() {
+ TelemetryIPCAccumulator::DispatchToMainThread(NS_NewRunnableFunction(
+ "TelemetryIPCAccumulator::IPCTimerFired", []() -> void {
+ TelemetryIPCAccumulator::IPCTimerFired(nullptr, nullptr);
+ }));
+}
+
+} // anonymous namespace
+
+////////////////////////////////////////////////////////////////////////
+////////////////////////////////////////////////////////////////////////
+//
+// EXTERNALLY VISIBLE FUNCTIONS in namespace TelemetryIPCAccumulator::
+
+void TelemetryIPCAccumulator::AccumulateChildHistogram(
+ mozilla::Telemetry::HistogramID aId, uint32_t aSample) {
+ StaticMutexAutoLock locker(gTelemetryIPCAccumulatorMutex);
+ if (!gHistogramAccumulations) {
+ gHistogramAccumulations = new nsTArray<HistogramAccumulation>();
+ }
+ if (gHistogramAccumulations->Length() >=
+ kWaterMarkDiscardFactor * kHistogramAccumulationsArrayHighWaterMark) {
+ gDiscardedData.mDiscardedHistogramAccumulations++;
+ return;
+ }
+ if (gHistogramAccumulations->Length() ==
+ kHistogramAccumulationsArrayHighWaterMark) {
+ DispatchIPCTimerFired();
+ }
+ gHistogramAccumulations->AppendElement(HistogramAccumulation{aId, aSample});
+ ArmIPCTimer(locker);
+}
+
+void TelemetryIPCAccumulator::AccumulateChildKeyedHistogram(
+ mozilla::Telemetry::HistogramID aId, const nsCString& aKey,
+ uint32_t aSample) {
+ StaticMutexAutoLock locker(gTelemetryIPCAccumulatorMutex);
+ if (!gKeyedHistogramAccumulations) {
+ gKeyedHistogramAccumulations = new nsTArray<KeyedHistogramAccumulation>();
+ }
+ if (gKeyedHistogramAccumulations->Length() >=
+ kWaterMarkDiscardFactor * kHistogramAccumulationsArrayHighWaterMark) {
+ gDiscardedData.mDiscardedKeyedHistogramAccumulations++;
+ return;
+ }
+ if (gKeyedHistogramAccumulations->Length() ==
+ kHistogramAccumulationsArrayHighWaterMark) {
+ DispatchIPCTimerFired();
+ }
+ gKeyedHistogramAccumulations->AppendElement(
+ KeyedHistogramAccumulation{aId, aSample, aKey});
+ ArmIPCTimer(locker);
+}
+
+void TelemetryIPCAccumulator::RecordChildScalarAction(
+ uint32_t aId, bool aDynamic, ScalarActionType aAction,
+ const ScalarVariant& aValue) {
+ StaticMutexAutoLock locker(gTelemetryIPCAccumulatorMutex);
+ // Make sure to have the storage.
+ if (!gChildScalarsActions) {
+ gChildScalarsActions = new nsTArray<ScalarAction>();
+ }
+ if (gChildScalarsActions->Length() >=
+ kWaterMarkDiscardFactor * kScalarActionsArrayHighWaterMark) {
+ gDiscardedData.mDiscardedScalarActions++;
+ return;
+ }
+ if (gChildScalarsActions->Length() == kScalarActionsArrayHighWaterMark) {
+ DispatchIPCTimerFired();
+ }
+ // Store the action. The ProcessID will be determined by the receiver.
+ gChildScalarsActions->AppendElement(ScalarAction{
+ aId, aDynamic, aAction, Some(aValue), Telemetry::ProcessID::Count});
+ ArmIPCTimer(locker);
+}
+
+void TelemetryIPCAccumulator::RecordChildKeyedScalarAction(
+ uint32_t aId, bool aDynamic, const nsAString& aKey,
+ ScalarActionType aAction, const ScalarVariant& aValue) {
+ StaticMutexAutoLock locker(gTelemetryIPCAccumulatorMutex);
+ // Make sure to have the storage.
+ if (!gChildKeyedScalarsActions) {
+ gChildKeyedScalarsActions = new nsTArray<KeyedScalarAction>();
+ }
+ if (gChildKeyedScalarsActions->Length() >=
+ kWaterMarkDiscardFactor * kScalarActionsArrayHighWaterMark) {
+ gDiscardedData.mDiscardedKeyedScalarActions++;
+ return;
+ }
+ if (gChildKeyedScalarsActions->Length() == kScalarActionsArrayHighWaterMark) {
+ DispatchIPCTimerFired();
+ }
+ // Store the action. The ProcessID will be determined by the receiver.
+ gChildKeyedScalarsActions->AppendElement(
+ KeyedScalarAction{aId, aDynamic, aAction, NS_ConvertUTF16toUTF8(aKey),
+ Some(aValue), Telemetry::ProcessID::Count});
+ ArmIPCTimer(locker);
+}
+
+void TelemetryIPCAccumulator::RecordChildEvent(
+ const mozilla::TimeStamp& timestamp, const nsACString& category,
+ const nsACString& method, const nsACString& object,
+ const mozilla::Maybe<nsCString>& value,
+ const nsTArray<mozilla::Telemetry::EventExtraEntry>& extra) {
+ StaticMutexAutoLock locker(gTelemetryIPCAccumulatorMutex);
+
+ if (!gChildEvents) {
+ gChildEvents = new nsTArray<ChildEventData>();
+ }
+
+ if (gChildEvents->Length() >=
+ kWaterMarkDiscardFactor * kEventsArrayHighWaterMark) {
+ gDiscardedData.mDiscardedChildEvents++;
+ return;
+ }
+
+ if (gChildEvents->Length() == kEventsArrayHighWaterMark) {
+ DispatchIPCTimerFired();
+ }
+
+ // Store the event.
+ gChildEvents->AppendElement(
+ ChildEventData{timestamp, nsCString(category), nsCString(method),
+ nsCString(object), value, extra.Clone()});
+ ArmIPCTimer(locker);
+}
+
+// This method takes the lock only to double-buffer the batched telemetry.
+// It releases the lock before calling out to IPC code which can (and does)
+// Accumulate (which would deadlock)
+template <class TActor>
+static void SendAccumulatedData(TActor* ipcActor) {
+ // Get the accumulated data and free the storage buffers.
+ nsTArray<HistogramAccumulation> histogramsToSend;
+ nsTArray<KeyedHistogramAccumulation> keyedHistogramsToSend;
+ nsTArray<ScalarAction> scalarsToSend;
+ nsTArray<KeyedScalarAction> keyedScalarsToSend;
+ nsTArray<ChildEventData> eventsToSend;
+ DiscardedData discardedData;
+
+ {
+ StaticMutexAutoLock locker(gTelemetryIPCAccumulatorMutex);
+ if (gHistogramAccumulations) {
+ histogramsToSend = std::move(*gHistogramAccumulations);
+ }
+ if (gKeyedHistogramAccumulations) {
+ keyedHistogramsToSend = std::move(*gKeyedHistogramAccumulations);
+ }
+ if (gChildScalarsActions) {
+ scalarsToSend = std::move(*gChildScalarsActions);
+ }
+ if (gChildKeyedScalarsActions) {
+ keyedScalarsToSend = std::move(*gChildKeyedScalarsActions);
+ }
+ if (gChildEvents) {
+ eventsToSend = std::move(*gChildEvents);
+ }
+ discardedData = gDiscardedData;
+ gDiscardedData = {0};
+ }
+
+ // Send the accumulated data to the parent process.
+ MOZ_ASSERT(ipcActor);
+ if (histogramsToSend.Length()) {
+ mozilla::Unused << NS_WARN_IF(
+ !ipcActor->SendAccumulateChildHistograms(histogramsToSend));
+ }
+ if (keyedHistogramsToSend.Length()) {
+ mozilla::Unused << NS_WARN_IF(
+ !ipcActor->SendAccumulateChildKeyedHistograms(keyedHistogramsToSend));
+ }
+ if (scalarsToSend.Length()) {
+ mozilla::Unused << NS_WARN_IF(
+ !ipcActor->SendUpdateChildScalars(scalarsToSend));
+ }
+ if (keyedScalarsToSend.Length()) {
+ mozilla::Unused << NS_WARN_IF(
+ !ipcActor->SendUpdateChildKeyedScalars(keyedScalarsToSend));
+ }
+ if (eventsToSend.Length()) {
+ mozilla::Unused << NS_WARN_IF(
+ !ipcActor->SendRecordChildEvents(eventsToSend));
+ }
+ mozilla::Unused << NS_WARN_IF(
+ !ipcActor->SendRecordDiscardedData(discardedData));
+}
+
+// To ensure we don't loop IPCTimerFired->AccumulateChild->arm timer, we don't
+// unset gIPCTimerArmed until the IPC completes
+//
+// This function must be called on the main thread, otherwise IPC will fail.
+void TelemetryIPCAccumulator::IPCTimerFired(nsITimer* aTimer, void* aClosure) {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ // Send accumulated data to the correct parent process.
+ switch (XRE_GetProcessType()) {
+ case GeckoProcessType_Content:
+ SendAccumulatedData(mozilla::dom::ContentChild::GetSingleton());
+ break;
+ case GeckoProcessType_GPU:
+ SendAccumulatedData(mozilla::gfx::GPUParent::GetSingleton());
+ break;
+ case GeckoProcessType_RDD:
+ SendAccumulatedData(mozilla::RDDParent::GetSingleton());
+ break;
+ case GeckoProcessType_Socket:
+ SendAccumulatedData(mozilla::net::SocketProcessChild::GetSingleton());
+ break;
+ case GeckoProcessType_Utility:
+ SendAccumulatedData(
+ mozilla::ipc::UtilityProcessChild::GetSingleton().get());
+ break;
+ default:
+ MOZ_ASSERT_UNREACHABLE("Unsupported process type");
+ break;
+ }
+
+ gIPCTimerArmed = false;
+}
+
+void TelemetryIPCAccumulator::DeInitializeGlobalState() {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ StaticMutexAutoLock locker(gTelemetryIPCAccumulatorMutex);
+ if (gIPCTimer) {
+ NS_RELEASE(gIPCTimer);
+ }
+
+ gHistogramAccumulations = nullptr;
+ gKeyedHistogramAccumulations = nullptr;
+ gChildScalarsActions = nullptr;
+ gChildKeyedScalarsActions = nullptr;
+ gChildEvents = nullptr;
+}
+
+void TelemetryIPCAccumulator::DispatchToMainThread(
+ already_AddRefed<nsIRunnable>&& aEvent) {
+ SchedulerGroup::Dispatch(std::move(aEvent));
+}
diff --git a/toolkit/components/telemetry/core/ipc/TelemetryIPCAccumulator.h b/toolkit/components/telemetry/core/ipc/TelemetryIPCAccumulator.h
new file mode 100644
index 0000000000..e77ca95391
--- /dev/null
+++ b/toolkit/components/telemetry/core/ipc/TelemetryIPCAccumulator.h
@@ -0,0 +1,56 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2; -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef TelemetryIPCAccumulator_h__
+#define TelemetryIPCAccumulator_h__
+
+#include "mozilla/AlreadyAddRefed.h"
+#include "mozilla/Maybe.h"
+#include "nsStringFwd.h"
+#include "mozilla/Telemetry.h" // for EventExtraEntry
+#include "mozilla/TelemetryComms.h" // for ScalarActionType, Scala...
+#include "mozilla/TelemetryHistogramEnums.h" // for HistogramID
+
+class nsIRunnable;
+class nsITimer;
+
+namespace mozilla {
+
+class TimeStamp;
+
+namespace TelemetryIPCAccumulator {
+
+// Histogram accumulation functions.
+void AccumulateChildHistogram(mozilla::Telemetry::HistogramID aId,
+ uint32_t aSample);
+void AccumulateChildKeyedHistogram(mozilla::Telemetry::HistogramID aId,
+ const nsCString& aKey, uint32_t aSample);
+
+// Scalar accumulation functions.
+void RecordChildScalarAction(uint32_t aId, bool aDynamic,
+ mozilla::Telemetry::ScalarActionType aAction,
+ const mozilla::Telemetry::ScalarVariant& aValue);
+
+void RecordChildKeyedScalarAction(
+ uint32_t aId, bool aDynamic, const nsAString& aKey,
+ mozilla::Telemetry::ScalarActionType aAction,
+ const mozilla::Telemetry::ScalarVariant& aValue);
+
+void RecordChildEvent(
+ const mozilla::TimeStamp& timestamp, const nsACString& category,
+ const nsACString& method, const nsACString& object,
+ const mozilla::Maybe<nsCString>& value,
+ const nsTArray<mozilla::Telemetry::EventExtraEntry>& extra);
+
+void IPCTimerFired(nsITimer* aTimer, void* aClosure);
+
+void DeInitializeGlobalState();
+
+void DispatchToMainThread(already_AddRefed<nsIRunnable>&& aEvent);
+
+} // namespace TelemetryIPCAccumulator
+} // namespace mozilla
+
+#endif // TelemetryIPCAccumulator_h__
diff --git a/toolkit/components/telemetry/core/nsITelemetry.idl b/toolkit/components/telemetry/core/nsITelemetry.idl
new file mode 100644
index 0000000000..ddad0ea7d4
--- /dev/null
+++ b/toolkit/components/telemetry/core/nsITelemetry.idl
@@ -0,0 +1,680 @@
+/* -*- Mode: C++; c-basic-offset: 2; indent-tabs-mode: nil; tab-width: 8 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "nsISupports.idl"
+#include "nsIFile.idl"
+
+[scriptable,function, uuid(3d3b9075-5549-4244-9c08-b64fefa1dd60)]
+interface nsIFetchTelemetryDataCallback : nsISupports
+{
+ void complete();
+};
+
+[scriptable, uuid(273d2dd0-6c63-475a-b864-cb65160a1909)]
+interface nsITelemetry : nsISupports
+{
+ /**
+ * Histogram types:
+ * HISTOGRAM_EXPONENTIAL - buckets increase exponentially
+ * HISTOGRAM_LINEAR - buckets increase linearly
+ * HISTOGRAM_BOOLEAN - For storing 0/1 values
+ * HISTOGRAM_FLAG - For storing a single value; its count is always == 1.
+ * HISTOGRAM_COUNT - For storing counter values without bucketing.
+ * HISTOGRAM_CATEGORICAL - For storing enumerated values by label.
+ */
+ const unsigned long HISTOGRAM_EXPONENTIAL = 0;
+ const unsigned long HISTOGRAM_LINEAR = 1;
+ const unsigned long HISTOGRAM_BOOLEAN = 2;
+ const unsigned long HISTOGRAM_FLAG = 3;
+ const unsigned long HISTOGRAM_COUNT = 4;
+ const unsigned long HISTOGRAM_CATEGORICAL = 5;
+
+ /**
+ * Scalar types:
+ * SCALAR_TYPE_COUNT - for storing a numeric value
+ * SCALAR_TYPE_STRING - for storing a string value
+ * SCALAR_TYPE_BOOLEAN - for storing a boolean value
+ */
+ const unsigned long SCALAR_TYPE_COUNT = 0;
+ const unsigned long SCALAR_TYPE_STRING = 1;
+ const unsigned long SCALAR_TYPE_BOOLEAN = 2;
+
+ /**
+ * Dataset types:
+ * DATASET_ALL_CHANNELS - the basic dataset that is on-by-default on all channels
+ * DATASET_PRERELEASE_CHANNELS - the extended dataset that is opt-in on release,
+ * opt-out on pre-release channels.
+ */
+ const unsigned long DATASET_ALL_CHANNELS = 0;
+ const unsigned long DATASET_PRERELEASE_CHANNELS = 1;
+
+ /**
+ * Serializes the histogram labels for categorical hitograms.
+ * The returned structure looks like:
+ * { "histogram1": [ "histogram1_label1", "histogram1_label2", ...],
+ * "histogram2": [ "histogram2_label1", "histogram2_label2", ...]
+ * ...
+ * }
+ *
+ * Note that this function should only be used in tests and about:telemetry.
+ */
+ [implicit_jscontext]
+ jsval getCategoricalLabels();
+
+ /**
+ * Serializes the histograms from the given store to a JSON-style object.
+ * The returned structure looks like:
+ * { "process": { "name1": histogramData1, "name2": histogramData2 }, ... }
+ *
+ * Each histogram is represented in a packed format and has the following properties:
+ * bucket_count - Number of buckets of this histogram
+ * histogram_type - HISTOGRAM_EXPONENTIAL, HISTOGRAM_LINEAR, HISTOGRAM_BOOLEAN,
+ * HISTOGRAM_FLAG, HISTOGRAM_COUNT, or HISTOGRAM_CATEGORICAL
+ * sum - sum of the bucket contents
+ * range - A 2-item array of minimum and maximum bucket size
+ * values - Map from bucket to the bucket's count
+ *
+ * @param aStoreName The name of the store to snapshot.
+ * Defaults to "main".
+ * Custom stores are available when probes have them defined.
+ * See the `record_into_store` attribute on histograms.
+ * @see https://firefox-source-docs.mozilla.org/toolkit/components/telemetry/telemetry/collection/histograms.html#record-into-store
+ * @param aClearStore Whether to clear out the histograms in the named store after snapshotting.
+ * Defaults to false.
+ * @param aFilterTest If true, `TELEMETRY_TEST_` histograms will be filtered out.
+ Filtered histograms are still cleared if `aClearStore` is true.
+ * Defaults to false.
+ */
+ [implicit_jscontext]
+ jsval getSnapshotForHistograms([optional] in ACString aStoreName, [optional] in boolean aClearStore, [optional] in boolean aFilterTest);
+
+ /**
+ * Serializes the keyed histograms from the given store to a JSON-style object.
+ * The returned structure looks like:
+ * { "process": { "name1": { "key_1": histogramData1, "key_2": histogramData2 }, ...}, ... }
+ *
+ * @param aStoreName The name of the store to snapshot.
+ * Defaults to "main".
+ * Custom stores are available when probes have them defined.
+ * See the `record_into_store` attribute on histograms.
+ * @see https://firefox-source-docs.mozilla.org/toolkit/components/telemetry/telemetry/collection/histograms.html#record-into-store
+ * @param aClearStore Whether to clear out the keyed histograms in the named store after snapshotting.
+ * Defaults to false.
+ * @param aFilterTest If true, `TELEMETRY_TEST_` histograms will be filtered out.
+ Filtered histograms are still cleared if `aClearStore` is true.
+ * Defaults to false.
+ */
+ [implicit_jscontext]
+ jsval getSnapshotForKeyedHistograms([optional] in ACString aStoreName, [optional] in boolean aClearStore, [optional] in boolean aFilterTest);
+
+ /**
+ * Serializes the scalars from the given store to a JSON-style object.
+ * The returned structure looks like:
+ * { "process": { "category1.probe": 1,"category1.other_probe": false, ... }, ... }.
+ *
+ * @param aStoreName The name of the store to snapshot.
+ * Defaults to "main".
+ * Custom stores are available when probes have them defined.
+ * See the `record_into_store` attribute on scalars.
+ * @see https://firefox-source-docs.mozilla.org/toolkit/components/telemetry/telemetry/collection/scalars.html#optional-fields
+ * @param aClearStore Whether to clear out the scalars in the named store after snapshotting.
+ * Defaults to false.
+ * @param aFilterTest If true, `telemetry.test` scalars will be filtered out.
+ Filtered scalars are still cleared if `aClearStore` is true.
+ * Defaults to false.
+ */
+ [implicit_jscontext]
+ jsval getSnapshotForScalars([optional] in ACString aStoreName, [optional] in boolean aClearStore, [optional] in boolean aFilterTest);
+
+ /**
+ * Serializes the keyed scalars from the given store to a JSON-style object.
+ * The returned structure looks like:
+ * { "process": { "category1.probe": { "key_1": 2, "key_2": 1, ... }, ... }, ... }
+ *
+ * @param aStoreName The name of the store to snapshot.
+ * Defaults to "main".
+ * Custom stores are available when probes have them defined.
+ * See the `record_into_store` attribute on scalars.
+ * @see https://firefox-source-docs.mozilla.org/toolkit/components/telemetry/telemetry/collection/scalars.html#optional-fields
+ * @param aClearStore Whether to clear out the keyed scalars in the named store after snapshotting.
+ * Defaults to false.
+ * @param aFilterTest If true, `telemetry.test` scalars will be filtered out.
+ Filtered scalars are still cleared if `aClearStore` is true.
+ * Defaults to false.
+ */
+ [implicit_jscontext]
+ jsval getSnapshotForKeyedScalars([optional] in ACString aStoreName, [optional] in boolean aClearStore, [optional] in boolean aFilterTest);
+
+ /**
+ * The amount of time, in milliseconds, that the last session took
+ * to shutdown. Reads as 0 to indicate failure.
+ */
+ readonly attribute uint32_t lastShutdownDuration;
+
+ /**
+ * The number of failed profile lock attempts that have occurred prior to
+ * successfully locking the profile
+ */
+ readonly attribute uint32_t failedProfileLockCount;
+
+ /*
+ * An object containing information about slow SQL statements.
+ *
+ * {
+ * mainThread: { "sqlString1": [<hit count>, <total time>], "sqlString2": [...], ... },
+ * otherThreads: { "sqlString3": [<hit count>, <total time>], "sqlString4": [...], ... }
+ * }
+ *
+ * where:
+ * mainThread: Slow statements that executed on the main thread
+ * otherThreads: Slow statements that executed on a non-main thread
+ * sqlString - String of the offending statement (see note)
+ * hit count - The number of times this statement required longer than the threshold time to execute
+ * total time - The sum of all execution times above the threshold time for this statement
+ *
+ * Note that dynamic SQL strings and SQL strings executed against addon DBs could contain private information.
+ * This property represents such SQL as aggregate database-level stats and the sqlString contains the database
+ * filename instead.
+ */
+ [implicit_jscontext]
+ readonly attribute jsval slowSQL;
+
+ /*
+ * See slowSQL above.
+ *
+ * An object containing full strings of every slow SQL statement if toolkit.telemetry.debugSlowSql = true
+ * The returned SQL strings may contain private information and should not be reported to Telemetry.
+ */
+ [implicit_jscontext]
+ readonly attribute jsval debugSlowSQL;
+
+ /**
+ * Flags for getUntrustedModuleLoadEvents.
+ */
+
+ /**
+ * This flag is set to retrieve all data including instances which have been
+ * retrieved before. If not set, only new instances since the last call
+ * will be returned.
+ * If this flag is set, KEEP_LOADEVENTS_NEW must not be set unless
+ * EXCLUDE_STACKINFO_FROM_LOADEVENTS is set.
+ * (See also MultiGetUntrustedModulesData::Serialize.)
+ */
+ const unsigned long INCLUDE_OLD_LOADEVENTS = 1 << 0;
+
+ /**
+ * This flag is set to keep the returned instances as if they were not
+ * retrieved, meaning those instances will be returned by a next method
+ * call without INCLUDE_OLD_LOADEVENTS. If not set, the returned instances
+ * can be re-retrieved only when INCLUDE_OLD_LOADEVENTS is specified.
+ * If this flag is set, INCLUDE_OLD_LOADEVENTS must not be set unless
+ * EXCLUDE_STACKINFO_FROM_LOADEVENTS is set.
+ * (See also MultiGetUntrustedModulesData::Serialize.)
+ */
+ const unsigned long KEEP_LOADEVENTS_NEW = 1 << 1;
+
+ /**
+ * This flag is set to include private fields.
+ * Do not specify this flag to retrieve data to be submitted.
+ */
+ const unsigned long INCLUDE_PRIVATE_FIELDS_IN_LOADEVENTS = 1 << 2;
+
+ /**
+ * This flag is set to exclude the "combinedStacks" field.
+ * Without this flag, the flags INCLUDE_OLD_LOADEVENTS and KEEP_LOADEVENTS_NEW
+ * cannot be set at the same time.
+ */
+ const unsigned long EXCLUDE_STACKINFO_FROM_LOADEVENTS = 1 << 3;
+
+ /*
+ * An array of untrusted module load events. Each element describes one or
+ * more modules that were loaded, contextual information at the time of the
+ * event (including stack trace), and flags describing the module's
+ * trustworthiness.
+ *
+ * @param aFlags Combination (bitwise OR) of the flags specified above.
+ * Defaults to 0.
+ */
+ [implicit_jscontext]
+ Promise getUntrustedModuleLoadEvents([optional] in unsigned long aFlags);
+
+ /*
+ * Whether the untrusted module load events are ready for processing.
+ * Calling getUntrustedModuleLoadEvents() before this attribute is true
+ * will result in an empty array. */
+ readonly attribute boolean areUntrustedModuleLoadEventsReady;
+
+ /*
+ * Asynchronously get an array of the modules loaded in the process.
+ *
+ * The data has the following structure:
+ *
+ * [
+ * {
+ * "name": <string>, // Name of the module file (e.g. xul.dll)
+ * "version": <string>, // Version of the module
+ * "debugName": <string>, // ID of the debug information file
+ * "debugID": <string>, // Name of the debug information file
+ * "certSubject": <string>, // Name of the organization that signed the binary (Optional, only defined when present)
+ * },
+ * ...
+ * ]
+ *
+ * @return A promise that resolves to an array of modules or rejects with
+ NS_ERROR_FAILURE on failure.
+ * @throws NS_ERROR_NOT_IMPLEMENTED if the Gecko profiler is not enabled.
+ */
+ [implicit_jscontext]
+ Promise getLoadedModules();
+
+ /*
+ * An object with two fields: memoryMap and stacks.
+ * * memoryMap is a list of loaded libraries.
+ * * stacks is a list of stacks. Each stack is a list of pairs of the form
+ * [moduleIndex, offset]. The moduleIndex is an index into the memoryMap and
+ * offset is an offset in the library at memoryMap[moduleIndex].
+ * This format is used to make it easier to send the stacks to the
+ * symbolication server.
+ */
+ [implicit_jscontext]
+ readonly attribute jsval lateWrites;
+
+ /**
+ * Create and return a histogram registered in TelemetryHistograms.h.
+ *
+ * @param id - unique identifier from TelemetryHistograms.h
+ * The returned object has the following functions:
+ * add(value) - Adds a sample of `value` to the histogram.
+ `value` may be a categorical histogram's label as a string,
+ a boolean histogram's value as a boolean,
+ or a number that fits inside a uint32_t.
+ * snapshot([optional] {store}) - Returns a snapshot of the histogram with the same data fields
+ as in getSnapshotForHistograms().
+ Defaults to the "main" store.
+ * clear([optional] {store}) - Zeros out the histogram's buckets and sum.
+ Defaults to the "main" store.
+ Note: This is intended to be only used in tests.
+ */
+ [implicit_jscontext]
+ jsval getHistogramById(in ACString id);
+
+ /**
+ * Create and return a histogram registered in TelemetryHistograms.h.
+ *
+ * @param id - unique identifier from TelemetryHistograms.h
+ * The returned object has the following functions:
+ * add(string key, [optional] value) - Adds a sample of `value` to the histogram for that key.
+ If no histogram for that key exists yet, it is created.
+ `value` may be a categorical histogram's label as a string,
+ a boolean histogram's value as a boolean,
+ or a number that fits inside a uint32_t.
+ * snapshot([optional] {store}) - Returns the snapshots of all the registered keys in the form
+ {key1: snapshot1, key2: snapshot2, ...} in the specified store.
+ * Defaults to the "main" store.
+ * keys([optional] {store}) - Returns an array with the string keys of the currently registered
+ histograms in the given store.
+ Defaults to "main".
+ * clear([optional] {store}) - Clears the registered histograms from this.
+ * Defaults to the "main" store.
+ * Note: This is intended to be only used in tests.
+ */
+ [implicit_jscontext]
+ jsval getKeyedHistogramById(in ACString id);
+
+ /**
+ * A flag indicating if Telemetry can record base data (FHR data). This is true if the
+ * FHR data reporting service or self-support are enabled.
+ *
+ * In the unlikely event that adding a new base probe is needed, please check the data
+ * collection wiki at https://wiki.mozilla.org/Firefox/Data_Collection and talk to the
+ * Telemetry team.
+ */
+ attribute boolean canRecordBase;
+
+ /**
+ * A flag indicating if Telemetry is allowed to record extended data. Returns false if
+ * the user hasn't opted into "extended Telemetry" on the Release channel, when the
+ * user has explicitly opted out of Telemetry on Nightly/Aurora/Beta or if manually
+ * set to false during tests.
+ *
+ * Set this to false in tests to disable gathering of extended telemetry statistics.
+ */
+ attribute boolean canRecordExtended;
+
+ /**
+ * A flag indicating whether Telemetry is recording release data, which is a
+ * smallish subset of our usage data that we're prepared to handle from our
+ * largish release population.
+ *
+ * This is true most of the time.
+ *
+ * This will always return true in the case of a non-content child process.
+ * Only values returned on the parent process are valid.
+ *
+ * This does not indicate whether Telemetry will send any data. That is
+ * governed by user preference and other mechanisms.
+ *
+ * You may use this to determine if it's okay to record your data.
+ */
+ readonly attribute boolean canRecordReleaseData;
+
+ /**
+ * A flag indicating whether Telemetry is recording prerelease data, which is
+ * a largish amount of usage data that we're prepared to handle from our
+ * smallish pre-release population.
+ *
+ * This is true on pre-release branches of Firefox.
+ *
+ * This does not indicate whether Telemetry will send any data. That is
+ * governed by user preference and other mechanisms.
+ *
+ * You may use this to determine if it's okay to record your data.
+ */
+ readonly attribute boolean canRecordPrereleaseData;
+
+ /**
+ * A flag indicating whether Telemetry can submit official results (for base or extended
+ * data). This is true on official, non-debug builds with built in support for Mozilla
+ * Telemetry reporting.
+ *
+ * This will always return true in the case of a non-content child process.
+ * Only values returned on the parent process are valid.
+ */
+ readonly attribute boolean isOfficialTelemetry;
+
+ /**
+ * Enable/disable recording for this histogram at runtime.
+ * Recording is enabled by default, unless listed at kRecordingInitiallyDisabledIDs[].
+ * Name must be a valid Histogram identifier, otherwise an assertion will be triggered.
+ *
+ * @param id - unique identifier from histograms.json
+ * @param enabled - whether or not to enable recording from now on.
+ */
+ void setHistogramRecordingEnabled(in ACString id, in boolean enabled);
+
+ /**
+ * Read data from the previous run. After the callback is called, the last
+ * shutdown time is available in lastShutdownDuration and any late
+ * writes in lateWrites.
+ */
+ void asyncFetchTelemetryData(in nsIFetchTelemetryDataCallback aCallback);
+
+ /**
+ * Get statistics of file IO reports, null, if not recorded.
+ *
+ * The statistics are returned as an object whose propoerties are the names
+ * of the files that have been accessed and whose corresponding values are
+ * arrays of size three, representing startup, normal, and shutdown stages.
+ * Each stage's entry is either null or an array with the layout
+ * [total_time, #creates, #reads, #writes, #fsyncs, #stats]
+ */
+ [implicit_jscontext]
+ readonly attribute jsval fileIOReports;
+
+ /**
+ * Return the number of milliseconds since process start using monotonic
+ * timestamps (unaffected by system clock changes). On Windows, this includes
+ * the period of time the device was suspended. On Linux and macOS, this does
+ * not include the period of time the device was suspneded.
+ */
+ double msSinceProcessStart();
+
+ /**
+ * Return the number of milliseconds since process start using monotonic
+ * timestamps (unaffected by system clock changes), including the periods of
+ * time the device was suspended.
+ * @throws NS_ERROR_NOT_AVAILABLE if unavailable.
+ */
+ double msSinceProcessStartIncludingSuspend();
+
+ /**
+ * Return the number of milliseconds since process start using monotonic
+ * timestamps (unaffected by system clock changes), excluding the periods of
+ * time the device was suspended.
+ * @throws NS_ERROR_NOT_AVAILABLE if unavailable.
+ */
+ double msSinceProcessStartExcludingSuspend();
+
+ /**
+ * Time since the system wide epoch. This is not a monotonic timer but
+ * can be used across process boundaries.
+ */
+ double msSystemNow();
+
+ /**
+ * Adds the value to the given scalar.
+ *
+ * @param aName The scalar name.
+ * @param aValue The numeric value to add to the scalar. Only unsigned integers supported.
+ */
+ [implicit_jscontext]
+ void scalarAdd(in ACString aName, in jsval aValue);
+
+ /**
+ * Sets the scalar to the given value.
+ *
+ * @param aName The scalar name.
+ * @param aValue The value to set the scalar to. If the type of aValue doesn't match the
+ * type of the scalar, the function will fail. For scalar string types, the this
+ * is truncated to 50 characters.
+ */
+ [implicit_jscontext]
+ void scalarSet(in ACString aName, in jsval aValue);
+
+ /**
+ * Sets the scalar to the maximum of the current and the passed value.
+ *
+ * @param aName The scalar name.
+ * @param aValue The numeric value to set the scalar to. Only unsigned integers supported.
+ */
+ [implicit_jscontext]
+ void scalarSetMaximum(in ACString aName, in jsval aValue);
+
+ /**
+ * Adds the value to the given keyed scalar.
+ *
+ * @param aName The scalar name.
+ * @param aKey The key name.
+ * @param aValue The numeric value to add to the scalar. Only unsigned integers supported.
+ */
+ [implicit_jscontext]
+ void keyedScalarAdd(in ACString aName, in AString aKey, in jsval aValue);
+
+ /**
+ * Sets the keyed scalar to the given value.
+ *
+ * @param aName The scalar name.
+ * @param aKey The key name.
+ * @param aValue The value to set the scalar to. If the type of aValue doesn't match the
+ * type of the scalar, the function will fail.
+ */
+ [implicit_jscontext]
+ void keyedScalarSet(in ACString aName, in AString aKey, in jsval aValue);
+
+ /**
+ * Sets the keyed scalar to the maximum of the current and the passed value.
+ *
+ * @param aName The scalar name.
+ * @param aKey The key name.
+ * @param aValue The numeric value to set the scalar to. Only unsigned integers supported.
+ */
+ [implicit_jscontext]
+ void keyedScalarSetMaximum(in ACString aName, in AString aKey, in jsval aValue);
+
+ /**
+ * Resets all the stored scalars. This is intended to be only used in tests.
+ */
+ void clearScalars();
+
+ /**
+ * Immediately sends any Telemetry batched on this process to the parent
+ * process. This is intended only to be used on process shutdown.
+ */
+ void flushBatchedChildTelemetry();
+
+ /**
+ * Record an event in Telemetry.
+ *
+ * @param aCategory The category name.
+ * @param aMethod The method name.
+ * @param aObject The object name.
+ * @param aValue An optional string value to record.
+ * @param aExtra An optional object of the form (string -> string).
+ * It should only contain registered extra keys.
+ *
+ * @throws NS_ERROR_INVALID_ARG When trying to record an unknown event.
+ */
+ [implicit_jscontext, optional_argc]
+ void recordEvent(in ACString aCategory, in ACString aMethod, in ACString aObject, [optional] in jsval aValue, [optional] in jsval extra);
+
+ /**
+ * Enable recording of events in a category.
+ * Events default to recording disabled. This allows to toggle recording for all events
+ * in the specified category.
+ *
+ * @param aCategory The category name.
+ * @param aEnabled Whether recording is enabled for events in that category.
+ */
+ void setEventRecordingEnabled(in ACString aCategory, in boolean aEnabled);
+
+ /**
+ * Serializes the recorded events to a JSON-appropriate array and optionally resets them.
+ * The returned structure looks like this:
+ * [
+ * // [timestamp, category, method, object, stringValue, extraValues]
+ * [43245, "category1", "method1", "object1", "string value", null],
+ * [43258, "category1", "method2", "object1", null, {"key1": "string value"}],
+ * ...
+ * ]
+ *
+ * @param aDataset DATASET_ALL_CHANNELS or DATASET_PRERELEASE_CHANNELS.
+ * @param [aClear=false] Whether to clear out the flushed events after snapshotting.
+ * @param aEventLimit How many events per process to limit the snapshot to contain, all if unspecified.
+ * Even if aClear, the leftover event records are not cleared.
+ */
+ [implicit_jscontext, optional_argc]
+ jsval snapshotEvents(in uint32_t aDataset, [optional] in boolean aClear, [optional] in uint32_t aEventLimit);
+
+ /**
+ * Register new events to record them from addons. This allows registering multiple
+ * events for a category. They will be valid only for the current Firefox session.
+ * Note that events shipping in Firefox should be registered in Events.yaml.
+ *
+ * @param aCategory The unique category the events are registered in.
+ * @param aEventData An object that contains registration data for 1-N events of the form:
+ * {
+ * "categoryName": {
+ * "methods": ["test1"],
+ * "objects": ["object1"],
+ * "record_on_release": false,
+ * "extra_keys": ["key1", "key2"], // optional
+ * "expired": false // optional, defaults to false.
+ * },
+ * ...
+ * }
+ * @param aEventData.<name>.methods List of methods for this event entry.
+ * @param aEventData.<name>.objects List of objects for this event entry.
+ * @param aEventData.<name>.extra_keys Optional, list of allowed extra keys for this event entry.
+ * @param aEventData.<name>.record_on_release Optional, whether to record this data on release.
+ * Defaults to false.
+ * @param aEventData.<name>.expired Optional, whether this event entry is expired. This allows
+ * recording it without error, but it will be discarded. Defaults to false.
+ */
+ [implicit_jscontext]
+ void registerEvents(in ACString aCategory, in jsval aEventData);
+
+ /**
+ * Parent process only. Register dynamic builtin events. The parameters
+ * have the same meaning as the usual |registerEvents| function.
+ *
+ * This function is only meant to be used to support the "artifact build"/
+ * "build faster" developers by allowing to add new events without rebuilding
+ * the C++ components including the headers files.
+ */
+ [implicit_jscontext]
+ void registerBuiltinEvents(in ACString aCategory, in jsval aEventData);
+
+ /**
+ * Parent process only. Register new scalars to record them from addons. This
+ * allows registering multiple scalars for a category. They will be valid only for
+ * the current Firefox session.
+ * Note that scalars shipping in Firefox should be registered in Scalars.yaml.
+ *
+ * @param aCategoryName The unique category the scalars are registered in.
+ * @param aScalarData An object that contains registration data for multiple scalars in the form:
+ * {
+ * "sample_scalar": {
+ * "kind": Ci.nsITelemetry.SCALAR_TYPE_COUNT,
+ * "keyed": true, //optional, defaults to false
+ * "record_on_release: true, // optional, defaults to false
+ * "expired": false // optional, defaults to false.
+ * },
+ * ...
+ * }
+ * @param aScalarData.<name>.kind One of the scalar types defined in this file (SCALAR_TYPE_*)
+ * @param aScalarData.<name>.keyed Optional, whether this is a keyed scalar or not. Defaults to false.
+ * @param aScalarData.<name>.record_on_release Optional, whether to record this data on release.
+ * Defaults to false.
+ * @param aScalarData.<name>.expired Optional, whether this scalar entry is expired. This allows
+ * recording it without error, but it will be discarded. Defaults to false.
+ */
+ [implicit_jscontext]
+ void registerScalars(in ACString aCategoryName, in jsval aScalarData);
+
+ /**
+ * Parent process only. Register dynamic builtin scalars. The parameters
+ * have the same meaning as the usual |registerScalars| function.
+ *
+ * This function is only meant to be used to support the "artifact build"/
+ * "build faster" developers by allowing to add new scalars without rebuilding
+ * the C++ components including the headers files.
+ */
+ [implicit_jscontext]
+ void registerBuiltinScalars(in ACString aCategoryName, in jsval aScalarData);
+
+ /**
+ * Resets all the stored events. This is intended to be only used in tests.
+ * Events recorded but not yet flushed to the parent process storage won't be cleared.
+ * Override the pref. `toolkit.telemetry.ipcBatchTimeout` to reduce the time to flush events.
+ */
+ void clearEvents();
+
+ /**
+ * Get a list of all registered stores.
+ *
+ * The list is deduplicated, but unordered.
+ */
+ [implicit_jscontext]
+ jsval getAllStores();
+
+ /**
+ * Does early, cheap initialization for native telemetry data providers.
+ * Currently, this includes only MemoryTelemetry.
+ */
+ void earlyInit();
+
+ /**
+ * Does late, expensive initialization for native telemetry data providers.
+ * Currently, this includes only MemoryTelemetry.
+ *
+ * This should only be called after startup has completed and the event loop
+ * is idle.
+ */
+ void delayedInit();
+
+ /**
+ * Shuts down native telemetry providers. Currently, this includes only
+ * MemoryTelemetry.
+ */
+ void shutdown();
+
+ /**
+ * Gathers telemetry data for memory usage and records it to the data store.
+ * Returns a promise which resolves when asynchronous data collection has
+ * completed and all data has been recorded.
+ */
+ [implicit_jscontext]
+ Promise gatherMemory();
+};