/* -*- 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 gIPCTimerArmed(false); mozilla::Atomic gIPCTimerArming(false); // This batches child process accumulations that should be sent to the parent. StaticAutoPtr> gHistogramAccumulations; StaticAutoPtr> gKeyedHistogramAccumulations; StaticAutoPtr> gChildScalarsActions; StaticAutoPtr> gChildKeyedScalarsActions; StaticAutoPtr> 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(); } 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(); } 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(); } 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(); } 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& value, const nsTArray& extra) { StaticMutexAutoLock locker(gTelemetryIPCAccumulatorMutex); if (!gChildEvents) { gChildEvents = new nsTArray(); } 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 static void SendAccumulatedData(TActor* ipcActor) { // Get the accumulated data and free the storage buffers. nsTArray histogramsToSend; nsTArray keyedHistogramsToSend; nsTArray scalarsToSend; nsTArray keyedScalarsToSend; nsTArray 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&& aEvent) { SchedulerGroup::Dispatch(TaskCategory::Other, std::move(aEvent)); }