diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 19:33:14 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 19:33:14 +0000 |
commit | 36d22d82aa202bb199967e9512281e9a53db42c9 (patch) | |
tree | 105e8c98ddea1c1e4784a60a5a6410fa416be2de /toolkit/components/telemetry/core | |
parent | Initial commit. (diff) | |
download | firefox-esr-36d22d82aa202bb199967e9512281e9a53db42c9.tar.xz firefox-esr-36d22d82aa202bb199967e9512281e9a53db42c9.zip |
Adding upstream version 115.7.0esr.upstream/115.7.0esrupstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'toolkit/components/telemetry/core')
24 files changed, 15405 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..98d9e5fa6d --- /dev/null +++ b/toolkit/components/telemetry/core/Stopwatch.cpp @@ -0,0 +1,750 @@ +/* -*- 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); + if (!JS::GetWeakMapEntry(aCx, objs, obj, &keysObj)) { + return nullptr; + } + if (!keysObj.isObject()) { + if (aCreate) { + keys = new TimerKeys(); + Unused << nsContentUtils::WrapNative(aCx, keys, &keysObj); + } + if (!keysObj.isObject() || !JS::SetWeakMapEntry(aCx, objs, obj, 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..101347664f --- /dev/null +++ b/toolkit/components/telemetry/core/Telemetry.cpp @@ -0,0 +1,2026 @@ +/* -*- 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 +} + +#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("signons.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(×tamp)))) { + 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(×tamp)))) { + 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..9a0ccf8969 --- /dev/null +++ b/toolkit/components/telemetry/core/TelemetryHistogram.cpp @@ -0,0 +1,3678 @@ +/* -*- 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) + : mStorage(), 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) + : mStorage(), + 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..82a624f4ec --- /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_AND_UTILITY_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..f645edd27d --- /dev/null +++ b/toolkit/components/telemetry/core/ipc/TelemetryIPCAccumulator.cpp @@ -0,0 +1,352 @@ +/* -*- 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::TaskCategory; +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(TaskCategory::Other, 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..1570287f38 --- /dev/null +++ b/toolkit/components/telemetry/core/nsITelemetry.idl @@ -0,0 +1,674 @@ +/* -*- 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); + + /* + * 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(); +}; |