diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-28 14:29:10 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-28 14:29:10 +0000 |
commit | 2aa4a82499d4becd2284cdb482213d541b8804dd (patch) | |
tree | b80bf8bf13c3766139fbacc530efd0dd9d54394c /xpcom/base/MemoryTelemetry.cpp | |
parent | Initial commit. (diff) | |
download | firefox-upstream.tar.xz firefox-upstream.zip |
Adding upstream version 86.0.1.upstream/86.0.1upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'xpcom/base/MemoryTelemetry.cpp')
-rw-r--r-- | xpcom/base/MemoryTelemetry.cpp | 509 |
1 files changed, 509 insertions, 0 deletions
diff --git a/xpcom/base/MemoryTelemetry.cpp b/xpcom/base/MemoryTelemetry.cpp new file mode 100644 index 0000000000..5f34845a36 --- /dev/null +++ b/xpcom/base/MemoryTelemetry.cpp @@ -0,0 +1,509 @@ +/* -*- 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 "MemoryTelemetry.h" +#include "nsMemoryReporterManager.h" + +#include "mozilla/ClearOnShutdown.h" +#include "mozilla/Result.h" +#include "mozilla/ResultExtensions.h" +#include "mozilla/Services.h" +#include "mozilla/ScopeExit.h" +#include "mozilla/SimpleEnumerator.h" +#include "mozilla/Telemetry.h" +#include "mozilla/TimeStamp.h" +#include "mozilla/dom/ContentParent.h" +#include "mozilla/dom/ScriptSettings.h" +#include "nsContentUtils.h" +#include "nsIBrowserDOMWindow.h" +#include "nsIDOMChromeWindow.h" +#include "nsIMemoryReporter.h" +#include "nsIWindowMediator.h" +#include "nsImportModule.h" +#include "nsITelemetry.h" +#include "nsNetCID.h" +#include "nsObserverService.h" +#include "nsReadableUtils.h" +#include "nsThreadUtils.h" +#include "nsXULAppAPI.h" +#include "xpcpublic.h" + +#include <cstdlib> + +using namespace mozilla; + +using mozilla::dom::AutoJSAPI; +using mozilla::dom::ContentParent; + +// Do not gather data more than once a minute (ms) +static constexpr uint32_t kTelemetryInterval = 60 * 1000; + +static constexpr const char* kTopicCycleCollectorBegin = + "cycle-collector-begin"; + +// How long to wait in millis for all the child memory reports to come in +static constexpr uint32_t kTotalMemoryCollectorTimeout = 200; + +namespace { + +enum class PrevValue : uint32_t { +#ifdef XP_WIN + LOW_MEMORY_EVENTS_VIRTUAL, + LOW_MEMORY_EVENTS_COMMIT_SPACE, + LOW_MEMORY_EVENTS_PHYSICAL, +#endif +#if defined(XP_LINUX) && !defined(ANDROID) + PAGE_FAULTS_HARD, +#endif + SIZE_, +}; + +} // anonymous namespace + +constexpr uint32_t kUninitialized = ~0; + +static uint32_t gPrevValues[uint32_t(PrevValue::SIZE_)]; + +static uint32_t PrevValueIndex(Telemetry::HistogramID aId) { + switch (aId) { +#ifdef XP_WIN + case Telemetry::LOW_MEMORY_EVENTS_VIRTUAL: + return uint32_t(PrevValue::LOW_MEMORY_EVENTS_VIRTUAL); + case Telemetry::LOW_MEMORY_EVENTS_COMMIT_SPACE: + return uint32_t(PrevValue::LOW_MEMORY_EVENTS_COMMIT_SPACE); + case Telemetry::LOW_MEMORY_EVENTS_PHYSICAL: + return uint32_t(PrevValue::LOW_MEMORY_EVENTS_PHYSICAL); +#endif +#if defined(XP_LINUX) && !defined(ANDROID) + case Telemetry::PAGE_FAULTS_HARD: + return uint32_t(PrevValue::PAGE_FAULTS_HARD); +#endif + default: + MOZ_ASSERT_UNREACHABLE("Unexpected histogram ID"); + return 0; + } +} + +NS_IMPL_ISUPPORTS(MemoryTelemetry, nsIObserver, nsISupportsWeakReference) + +MemoryTelemetry::MemoryTelemetry() + : mThreadPool(do_GetService(NS_STREAMTRANSPORTSERVICE_CONTRACTID)) {} + +void MemoryTelemetry::Init() { + for (auto& val : gPrevValues) { + val = kUninitialized; + } + + if (XRE_IsContentProcess()) { + nsCOMPtr<nsIObserverService> obs = services::GetObserverService(); + MOZ_RELEASE_ASSERT(obs); + + obs->AddObserver(this, "content-child-shutdown", true); + } +} + +/* static */ MemoryTelemetry& MemoryTelemetry::Get() { + static RefPtr<MemoryTelemetry> sInstance; + + MOZ_ASSERT(NS_IsMainThread()); + + if (!sInstance) { + sInstance = new MemoryTelemetry(); + sInstance->Init(); + ClearOnShutdown(&sInstance); + } + return *sInstance; +} + +nsresult MemoryTelemetry::DelayedInit() { + if (Telemetry::CanRecordExtended()) { + nsCOMPtr<nsIObserverService> obs = services::GetObserverService(); + MOZ_RELEASE_ASSERT(obs); + + obs->AddObserver(this, kTopicCycleCollectorBegin, true); + } + + GatherReports(); + + return NS_OK; +} + +nsresult MemoryTelemetry::Shutdown() { + nsCOMPtr<nsIObserverService> obs = services::GetObserverService(); + MOZ_RELEASE_ASSERT(obs); + + obs->RemoveObserver(this, kTopicCycleCollectorBegin); + + return NS_OK; +} + +static inline void HandleMemoryReport(Telemetry::HistogramID aId, + int32_t aUnits, uint64_t aAmount, + const nsCString& aKey = VoidCString()) { + uint32_t val; + switch (aUnits) { + case nsIMemoryReporter::UNITS_BYTES: + val = uint32_t(aAmount / 1024); + break; + + case nsIMemoryReporter::UNITS_PERCENTAGE: + // UNITS_PERCENTAGE amounts are 100x greater than their raw value. + val = uint32_t(aAmount / 100); + break; + + case nsIMemoryReporter::UNITS_COUNT: + val = uint32_t(aAmount); + break; + + case nsIMemoryReporter::UNITS_COUNT_CUMULATIVE: { + // If the reporter gives us a cumulative count, we'll report the + // difference in its value between now and our previous ping. + + uint32_t idx = PrevValueIndex(aId); + uint32_t prev = gPrevValues[idx]; + gPrevValues[idx] = aAmount; + + if (prev == kUninitialized) { + // If this is the first time we're reading this reporter, store its + // current value but don't report it in the telemetry ping, so we + // ignore the effect startup had on the reporter. + return; + } + val = aAmount - prev; + break; + } + + default: + MOZ_ASSERT_UNREACHABLE("Unexpected aUnits value"); + return; + } + + // Note: The reference equality check here should allow the compiler to + // optimize this case out at compile time when we weren't given a key, + // while IsEmpty() or IsVoid() most likely will not. + if (&aKey == &VoidCString()) { + Telemetry::Accumulate(aId, val); + } else { + Telemetry::Accumulate(aId, aKey, val); + } +} + +nsresult MemoryTelemetry::GatherReports( + const std::function<void()>& aCompletionCallback) { + auto cleanup = MakeScopeExit([&]() { + if (aCompletionCallback) { + aCompletionCallback(); + } + }); + + RefPtr<nsMemoryReporterManager> mgr = nsMemoryReporterManager::GetOrCreate(); + MOZ_DIAGNOSTIC_ASSERT(mgr); + NS_ENSURE_TRUE(mgr, NS_ERROR_FAILURE); + +#define RECORD(id, metric, units) \ + do { \ + int64_t amt; \ + nsresult rv = mgr->Get##metric(&amt); \ + if (NS_SUCCEEDED(rv)) { \ + HandleMemoryReport(Telemetry::id, nsIMemoryReporter::units, amt); \ + } else if (rv != NS_ERROR_NOT_AVAILABLE) { \ + NS_WARNING("Failed to retrieve memory telemetry for " #metric); \ + } \ + } while (0) + + // GHOST_WINDOWS is opt-out as of Firefox 55 + RECORD(GHOST_WINDOWS, GhostWindows, UNITS_COUNT); + + // If we're running in the parent process, collect data from all processes for + // the MEMORY_TOTAL histogram. + if (XRE_IsParentProcess() && !mTotalMemoryGatherer) { + mTotalMemoryGatherer = new TotalMemoryGatherer(); + mTotalMemoryGatherer->Begin(mThreadPool); + } + + if (!Telemetry::CanRecordReleaseData()) { + return NS_OK; + } + + // Get memory measurements from distinguished amount attributes. We used + // to measure "explicit" too, but it could cause hangs, and the data was + // always really noisy anyway. See bug 859657. + // + // test_TelemetrySession.js relies on some of these histograms being + // here. If you remove any of the following histograms from here, you'll + // have to modify test_TelemetrySession.js: + // + // * MEMORY_TOTAL, + // * MEMORY_JS_GC_HEAP, and + // * MEMORY_JS_COMPARTMENTS_SYSTEM. + // + // The distinguished amount attribute names don't match the telemetry id + // names in some cases due to a combination of (a) historical reasons, and + // (b) the fact that we can't change telemetry id names without breaking + // data continuity. + + // Collect cheap or main-thread only metrics synchronously, on the main + // thread. + RECORD(MEMORY_JS_GC_HEAP, JSMainRuntimeGCHeap, UNITS_BYTES); + RECORD(MEMORY_JS_COMPARTMENTS_SYSTEM, JSMainRuntimeCompartmentsSystem, + UNITS_COUNT); + RECORD(MEMORY_JS_COMPARTMENTS_USER, JSMainRuntimeCompartmentsUser, + UNITS_COUNT); + RECORD(MEMORY_JS_REALMS_SYSTEM, JSMainRuntimeRealmsSystem, UNITS_COUNT); + RECORD(MEMORY_JS_REALMS_USER, JSMainRuntimeRealmsUser, UNITS_COUNT); + RECORD(MEMORY_IMAGES_CONTENT_USED_UNCOMPRESSED, ImagesContentUsedUncompressed, + UNITS_BYTES); + RECORD(MEMORY_STORAGE_SQLITE, StorageSQLite, UNITS_BYTES); +#ifdef XP_WIN + RECORD(LOW_MEMORY_EVENTS_VIRTUAL, LowMemoryEventsVirtual, + UNITS_COUNT_CUMULATIVE); + RECORD(LOW_MEMORY_EVENTS_COMMIT_SPACE, LowMemoryEventsCommitSpace, + UNITS_COUNT_CUMULATIVE); + RECORD(LOW_MEMORY_EVENTS_PHYSICAL, LowMemoryEventsPhysical, + UNITS_COUNT_CUMULATIVE); +#endif +#if defined(XP_LINUX) && !defined(ANDROID) + RECORD(PAGE_FAULTS_HARD, PageFaultsHard, UNITS_COUNT_CUMULATIVE); +#endif + + RefPtr<Runnable> completionRunnable; + if (aCompletionCallback) { + completionRunnable = NS_NewRunnableFunction(__func__, aCompletionCallback); + } + + // Collect expensive metrics that can be calculated off-main-thread + // asynchronously, on a background thread. + RefPtr<Runnable> runnable = NS_NewRunnableFunction( + "MemoryTelemetry::GatherReports", [mgr, completionRunnable]() mutable { + RECORD(MEMORY_VSIZE, Vsize, UNITS_BYTES); +#if !defined(HAVE_64BIT_BUILD) || !defined(XP_WIN) + RECORD(MEMORY_VSIZE_MAX_CONTIGUOUS, VsizeMaxContiguous, UNITS_BYTES); +#endif + RECORD(MEMORY_RESIDENT_FAST, ResidentFast, UNITS_BYTES); + RECORD(MEMORY_RESIDENT_PEAK, ResidentPeak, UNITS_BYTES); + RECORD(MEMORY_UNIQUE, ResidentUnique, UNITS_BYTES); + RECORD(MEMORY_HEAP_ALLOCATED, HeapAllocated, UNITS_BYTES); + RECORD(MEMORY_HEAP_OVERHEAD_FRACTION, HeapOverheadFraction, + UNITS_PERCENTAGE); + + if (completionRunnable) { + NS_DispatchToMainThread(completionRunnable.forget(), + NS_DISPATCH_NORMAL); + } + }); + +#undef RECORD + + nsresult rv = mThreadPool->Dispatch(runnable.forget(), NS_DISPATCH_NORMAL); + if (!NS_WARN_IF(NS_FAILED(rv))) { + cleanup.release(); + } + + return NS_OK; +} + +NS_IMPL_ISUPPORTS(MemoryTelemetry::TotalMemoryGatherer, nsITimerCallback) + +/** + * Polls all child processes for their unique set size, and populates the + * MEMORY_TOTAL and MEMORY_DISTRIBUTION_AMONG_CONTENT histograms with the + * results. + */ +void MemoryTelemetry::TotalMemoryGatherer::Begin(nsIEventTarget* aThreadPool) { + nsCOMPtr<nsISerialEventTarget> target = GetMainThreadSerialEventTarget(); + + nsTArray<ContentParent*> parents; + ContentParent::GetAll(parents); + for (auto& parent : parents) { + mRemainingChildCount++; + parent->SendGetMemoryUniqueSetSize()->Then( + target, "TotalMemoryGather::Begin", this, + &TotalMemoryGatherer::CollectResult, &TotalMemoryGatherer::OnFailure); + } + + mChildSizes.SetCapacity(mRemainingChildCount); + + RefPtr<TotalMemoryGatherer> self{this}; + + aThreadPool->Dispatch( + NS_NewRunnableFunction( + "TotalMemoryGather::Begin", + [self]() { + RefPtr<nsMemoryReporterManager> mgr = + nsMemoryReporterManager::GetOrCreate(); + MOZ_RELEASE_ASSERT(mgr); + + NS_DispatchToMainThread(NewRunnableMethod<int64_t>( + "TotalMemoryGather::CollectParentSize", self, + &TotalMemoryGatherer::CollectParentSize, mgr->ResidentFast())); + }), + NS_DISPATCH_NORMAL); + + NS_NewTimerWithCallback(getter_AddRefs(mTimeout), this, + kTotalMemoryCollectorTimeout, + nsITimer::TYPE_ONE_SHOT); +} + +nsresult MemoryTelemetry::TotalMemoryGatherer::MaybeFinish() { + // If we timed out waiting for a response from any child, we don't report + // anything for this attempt. + if (!mTimeout || !mHaveParentSize || mRemainingChildCount) { + return NS_OK; + } + + mTimeout = nullptr; + MemoryTelemetry::Get().mTotalMemoryGatherer = nullptr; + + HandleMemoryReport(Telemetry::MEMORY_TOTAL, nsIMemoryReporter::UNITS_BYTES, + mTotalMemory); + + if (mChildSizes.Length() > 1) { + int32_t tabsCount; + MOZ_TRY_VAR(tabsCount, GetOpenTabsCount()); + + nsCString key; + if (tabsCount <= 10) { + key = "0 - 10 tabs"; + } else if (tabsCount <= 500) { + key = "11 - 500 tabs"; + } else { + key = "more tabs"; + } + + // Mean of the USS of all the content processes. + int64_t mean = 0; + for (auto size : mChildSizes) { + mean += size; + } + mean /= mChildSizes.Length(); + + // For some users, for unknown reasons (though most likely because they're + // in a sandbox without procfs mounted), we wind up with 0 here, which + // triggers a floating point exception if we try to calculate values using + // it. + if (!mean) { + return NS_ERROR_UNEXPECTED; + } + + // Absolute error of USS for each content process, normalized by the mean + // (*100 to get it in percentage). 20% means for a content process that it + // is using 20% more or 20% less than the mean. + for (auto size : mChildSizes) { + int64_t diff = llabs(size - mean) * 100 / mean; + + HandleMemoryReport(Telemetry::MEMORY_DISTRIBUTION_AMONG_CONTENT, + nsIMemoryReporter::UNITS_COUNT, diff, key); + } + } + + // This notification is for testing only. + if (nsCOMPtr<nsIObserverService> obs = services::GetObserverService()) { + obs->NotifyObservers(nullptr, "gather-memory-telemetry-finished", nullptr); + } + + return NS_OK; +} + +void MemoryTelemetry::TotalMemoryGatherer::CollectParentSize( + int64_t aResident) { + mTotalMemory += aResident; + mHaveParentSize = true; + + MaybeFinish(); +} + +void MemoryTelemetry::TotalMemoryGatherer::CollectResult(int64_t aChildUSS) { + mChildSizes.AppendElement(aChildUSS); + + mTotalMemory += aChildUSS; + mRemainingChildCount--; + + MaybeFinish(); +} + +void MemoryTelemetry::TotalMemoryGatherer::OnFailure( + mozilla::ipc::ResponseRejectReason aReason) { + // Treat failure of any request the same as a timeout. + Notify(nullptr); +} + +nsresult MemoryTelemetry::TotalMemoryGatherer::Notify(nsITimer* aTimer) { + // Set mTimeout null to indicate the timeout has fired. After this, all + // results for this attempt will be ignored. + mTimeout = nullptr; + MemoryTelemetry::Get().mTotalMemoryGatherer = nullptr; + return NS_OK; +} + +/* static */ Result<uint32_t, nsresult> MemoryTelemetry::GetOpenTabsCount() { + nsresult rv; + + nsCOMPtr<nsIWindowMediator> windowMediator( + do_GetService(NS_WINDOWMEDIATOR_CONTRACTID, &rv)); + MOZ_TRY(rv); + + nsCOMPtr<nsISimpleEnumerator> enumerator; + MOZ_TRY(windowMediator->GetEnumerator(u"navigator:browser", + getter_AddRefs(enumerator))); + + uint32_t total = 0; + for (auto& window : SimpleEnumerator<nsIDOMChromeWindow>(enumerator)) { + nsCOMPtr<nsIBrowserDOMWindow> browserWin; + MOZ_TRY(window->GetBrowserDOMWindow(getter_AddRefs(browserWin))); + + NS_ENSURE_TRUE(browserWin, Err(NS_ERROR_UNEXPECTED)); + + uint32_t tabCount; + MOZ_TRY(browserWin->GetTabCount(&tabCount)); + total += tabCount; + } + + return total; +} + +void MemoryTelemetry::GetUniqueSetSize( + std::function<void(const int64_t&)>&& aCallback) { + mThreadPool->Dispatch( + NS_NewRunnableFunction( + "MemoryTelemetry::GetUniqueSetSize", + [callback = std::move(aCallback)]() mutable { + RefPtr<nsMemoryReporterManager> mgr = + nsMemoryReporterManager::GetOrCreate(); + MOZ_RELEASE_ASSERT(mgr); + + int64_t uss = mgr->ResidentUnique(); + + NS_DispatchToMainThread(NS_NewRunnableFunction( + "MemoryTelemetry::GetUniqueSetSizeResult", + [uss, callback = std::move(callback)]() { callback(uss); })); + }), + NS_DISPATCH_NORMAL); +} + +nsresult MemoryTelemetry::Observe(nsISupports* aSubject, const char* aTopic, + const char16_t* aData) { + if (strcmp(aTopic, kTopicCycleCollectorBegin) == 0) { + auto now = TimeStamp::Now(); + if (!mLastPoll.IsNull() && + (now - mLastPoll).ToMilliseconds() < kTelemetryInterval) { + return NS_OK; + } + + mLastPoll = now; + + NS_DispatchToCurrentThreadQueue( + NewRunnableMethod<std::function<void()>>( + "MemoryTelemetry::GatherReports", this, + &MemoryTelemetry::GatherReports, nullptr), + EventQueuePriority::Idle); + } else if (strcmp(aTopic, "content-child-shutdown") == 0) { + if (nsCOMPtr<nsITelemetry> telemetry = + do_GetService("@mozilla.org/base/telemetry;1")) { + telemetry->FlushBatchedChildTelemetry(); + } + } + return NS_OK; +} |