/* -*- 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 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"; 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 obs = services::GetObserverService(); MOZ_RELEASE_ASSERT(obs); obs->AddObserver(this, "content-child-shutdown", true); } } /* static */ MemoryTelemetry& MemoryTelemetry::Get() { static RefPtr sInstance; MOZ_ASSERT(NS_IsMainThread()); if (!sInstance) { sInstance = new MemoryTelemetry(); sInstance->Init(); ClearOnShutdown(&sInstance); } return *sInstance; } nsresult MemoryTelemetry::DelayedInit() { if (Telemetry::CanRecordExtended()) { nsCOMPtr obs = services::GetObserverService(); MOZ_RELEASE_ASSERT(obs); obs->AddObserver(this, kTopicCycleCollectorBegin, true); } GatherReports(); return NS_OK; } nsresult MemoryTelemetry::Shutdown() { nsCOMPtr 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& aCompletionCallback) { auto cleanup = MakeScopeExit([&]() { if (aCompletionCallback) { aCompletionCallback(); } }); RefPtr 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() && !mGatheringTotalMemory) { GatherTotalMemory(); } 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_PHYSICAL, LowMemoryEventsPhysical, UNITS_COUNT_CUMULATIVE); #endif #if defined(XP_LINUX) && !defined(ANDROID) RECORD(PAGE_FAULTS_HARD, PageFaultsHard, UNITS_COUNT_CUMULATIVE); #endif RefPtr 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 = NS_NewRunnableFunction( "MemoryTelemetry::GatherReports", [mgr, completionRunnable]() mutable { Telemetry::AutoTimer autoTimer; 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); // Although we can measure unique memory on MacOS we choose not to, because // doing so is too slow for telemetry. #ifndef XP_MACOSX RECORD(MEMORY_UNIQUE, ResidentUnique, UNITS_BYTES); #endif 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; } namespace { struct ChildProcessInfo { GeckoProcessType mType; #if defined(XP_WIN) HANDLE mHandle; #elif defined(XP_MACOSX) task_t mHandle; #else pid_t mHandle; #endif }; } // namespace /** * Runs a task on the background thread pool to fetch the memory usage of all * processes. */ void MemoryTelemetry::GatherTotalMemory() { MOZ_ASSERT(!mGatheringTotalMemory); mGatheringTotalMemory = true; nsTArray infos; mozilla::ipc::GeckoChildProcessHost::GetAll( [&](mozilla::ipc::GeckoChildProcessHost* aGeckoProcess) { if (!aGeckoProcess->GetChildProcessHandle()) { return; } ChildProcessInfo info{}; info.mType = aGeckoProcess->GetProcessType(); // NOTE: For now we ignore non-content processes here for compatibility // with the existing probe. We may want to introduce a new probe in the // future which also collects data for non-content processes. if (info.mType != GeckoProcessType_Content) { return; } #if defined(XP_WIN) if (!::DuplicateHandle(::GetCurrentProcess(), aGeckoProcess->GetChildProcessHandle(), ::GetCurrentProcess(), &info.mHandle, 0, false, DUPLICATE_SAME_ACCESS)) { return; } #elif defined(XP_MACOSX) info.mHandle = aGeckoProcess->GetChildTask(); if (mach_port_mod_refs(mach_task_self(), info.mHandle, MACH_PORT_RIGHT_SEND, 1) != KERN_SUCCESS) { return; } #else info.mHandle = base::GetProcId(aGeckoProcess->GetChildProcessHandle()); #endif infos.AppendElement(info); }); mThreadPool->Dispatch(NS_NewRunnableFunction( "MemoryTelemetry::GatherTotalMemory", [infos = std::move(infos)] { RefPtr mgr = nsMemoryReporterManager::GetOrCreate(); MOZ_RELEASE_ASSERT(mgr); int64_t totalMemory = mgr->ResidentFast(); nsTArray childSizes(infos.Length()); // Use our handle for the remote process to collect resident unique set // size information for that process. for (const auto& info : infos) { #ifdef XP_MACOSX int64_t memory = nsMemoryReporterManager::PhysicalFootprint(info.mHandle); #else int64_t memory = nsMemoryReporterManager::ResidentUnique(info.mHandle); #endif if (memory > 0) { childSizes.AppendElement(memory); totalMemory += memory; } #if defined(XP_WIN) ::CloseHandle(info.mHandle); #elif defined(XP_MACOSX) mach_port_deallocate(mach_task_self(), info.mHandle); #endif } NS_DispatchToMainThread(NS_NewRunnableFunction( "MemoryTelemetry::FinishGatheringTotalMemory", [totalMemory, childSizes = std::move(childSizes)] { MemoryTelemetry::Get().FinishGatheringTotalMemory(totalMemory, childSizes); })); })); } nsresult MemoryTelemetry::FinishGatheringTotalMemory( int64_t aTotalMemory, const nsTArray& aChildSizes) { mGatheringTotalMemory = false; // Total memory usage can be difficult to measure both accurately and fast // enough for telemetry (iterating memory maps can jank whole processes on // MacOS). Therefore this shouldn't be relied on as an absolute measurement // especially on MacOS where it double-counts shared memory. For a more // detailed explaination see: // https://groups.google.com/a/mozilla.org/g/dev-platform/c/WGNOtjHdsdA HandleMemoryReport(Telemetry::MEMORY_TOTAL, nsIMemoryReporter::UNITS_BYTES, aTotalMemory); if (aChildSizes.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 : aChildSizes) { mean += size; } mean /= aChildSizes.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 : aChildSizes) { 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 obs = services::GetObserverService()) { obs->NotifyObservers(nullptr, "gather-memory-telemetry-finished", nullptr); } return NS_OK; } /* static */ Result MemoryTelemetry::GetOpenTabsCount() { nsresult rv; nsCOMPtr windowMediator( do_GetService(NS_WINDOWMEDIATOR_CONTRACTID, &rv)); MOZ_TRY(rv); nsCOMPtr enumerator; MOZ_TRY(windowMediator->GetEnumerator(u"navigator:browser", getter_AddRefs(enumerator))); uint32_t total = 0; for (auto& window : SimpleEnumerator(enumerator)) { nsCOMPtr 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; } 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>( "MemoryTelemetry::GatherReports", this, &MemoryTelemetry::GatherReports, nullptr), EventQueuePriority::Idle); } else if (strcmp(aTopic, "content-child-shutdown") == 0) { if (nsCOMPtr telemetry = do_GetService("@mozilla.org/base/telemetry;1")) { telemetry->FlushBatchedChildTelemetry(); } } return NS_OK; }