diff options
Diffstat (limited to '')
-rw-r--r-- | xpcom/base/AvailableMemoryWatcherLinux.cpp | 282 |
1 files changed, 282 insertions, 0 deletions
diff --git a/xpcom/base/AvailableMemoryWatcherLinux.cpp b/xpcom/base/AvailableMemoryWatcherLinux.cpp new file mode 100644 index 0000000000..04927b7d46 --- /dev/null +++ b/xpcom/base/AvailableMemoryWatcherLinux.cpp @@ -0,0 +1,282 @@ +/* -*- 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 "AvailableMemoryWatcher.h" +#include "AvailableMemoryWatcherUtils.h" +#include "mozilla/Services.h" +#include "mozilla/StaticPrefs_browser.h" +#include "mozilla/Unused.h" +#include "nsAppRunner.h" +#include "nsIObserverService.h" +#include "nsISupports.h" +#include "nsITimer.h" +#include "nsIThread.h" +#include "nsMemoryPressure.h" + +namespace mozilla { + +// Linux has no native low memory detection. This class creates a timer that +// polls for low memory and sends a low memory notification if it notices a +// memory pressure event. +class nsAvailableMemoryWatcher final : public nsITimerCallback, + public nsINamed, + public nsAvailableMemoryWatcherBase { + public: + NS_DECL_ISUPPORTS_INHERITED + NS_DECL_NSITIMERCALLBACK + NS_DECL_NSIOBSERVER + NS_DECL_NSINAMED + + nsresult Init() override; + nsAvailableMemoryWatcher(); + + void HandleLowMemory(); + void MaybeHandleHighMemory(); + + private: + ~nsAvailableMemoryWatcher() = default; + void StartPolling(const MutexAutoLock&); + void StopPolling(const MutexAutoLock&); + void ShutDown(); + void UpdateCrashAnnotation(const MutexAutoLock&); + static bool IsMemoryLow(); + + nsCOMPtr<nsITimer> mTimer MOZ_GUARDED_BY(mMutex); + nsCOMPtr<nsIThread> mThread MOZ_GUARDED_BY(mMutex); + + bool mPolling MOZ_GUARDED_BY(mMutex); + bool mUnderMemoryPressure MOZ_GUARDED_BY(mMutex); + + // Polling interval to check for low memory. In high memory scenarios, + // default to 5000 ms between each check. + static const uint32_t kHighMemoryPollingIntervalMS = 5000; + + // Polling interval to check for low memory. Default to 1000 ms between each + // check. Use this interval when memory is low, + static const uint32_t kLowMemoryPollingIntervalMS = 1000; +}; + +// A modern version of linux should keep memory information in the +// /proc/meminfo path. +static const char* kMeminfoPath = "/proc/meminfo"; + +nsAvailableMemoryWatcher::nsAvailableMemoryWatcher() + : mPolling(false), mUnderMemoryPressure(false) {} + +nsresult nsAvailableMemoryWatcher::Init() { + nsresult rv = nsAvailableMemoryWatcherBase::Init(); + if (NS_FAILED(rv)) { + return rv; + } + MutexAutoLock lock(mMutex); + mTimer = NS_NewTimer(); + nsCOMPtr<nsIThread> thread; + // We have to make our own thread here instead of using the background pool, + // because some low memory scenarios can cause the background pool to fill. + rv = NS_NewNamedThread("MemoryPoller", getter_AddRefs(thread)); + if (NS_FAILED(rv)) { + NS_WARNING("Couldn't make a thread for nsAvailableMemoryWatcher."); + // In this scenario we can't poll for low memory, since we can't dispatch + // to our memory watcher thread. + return rv; + } + mThread = thread; + + // Set the crash annotation to its initial state. + UpdateCrashAnnotation(lock); + + StartPolling(lock); + + return NS_OK; +} + +already_AddRefed<nsAvailableMemoryWatcherBase> CreateAvailableMemoryWatcher() { + RefPtr watcher(new nsAvailableMemoryWatcher); + + if (NS_FAILED(watcher->Init())) { + return do_AddRef(new nsAvailableMemoryWatcherBase); + } + + return watcher.forget(); +} + +NS_IMPL_ISUPPORTS_INHERITED(nsAvailableMemoryWatcher, + nsAvailableMemoryWatcherBase, nsITimerCallback, + nsIObserver, nsINamed); + +void nsAvailableMemoryWatcher::StopPolling(const MutexAutoLock&) + MOZ_REQUIRES(mMutex) { + if (mPolling && mTimer) { + // stop dispatching memory checks to the thread. + mTimer->Cancel(); + mPolling = false; + } +} + +// Check /proc/meminfo for low memory. Largely C method for reading +// /proc/meminfo. +/* static */ +bool nsAvailableMemoryWatcher::IsMemoryLow() { + MemoryInfo memInfo{0, 0}; + nsresult rv = ReadMemoryFile(kMeminfoPath, memInfo); + + if (NS_FAILED(rv) || (memInfo.memAvailable == 0) || (memInfo.memTotal == 0)) { + // If memAvailable cannot be found, then we are using an older system. + // We can't accurately poll on this. + // If memTotal is zero we can't calculate how much memory we're using. + return false; + } + + unsigned long memoryAsPercentage = + (memInfo.memAvailable * 100) / memInfo.memTotal; + + return memoryAsPercentage <= + StaticPrefs::browser_low_commit_space_threshold_percent() || + memInfo.memAvailable < + StaticPrefs::browser_low_commit_space_threshold_mb() * 1024; +} + +void nsAvailableMemoryWatcher::ShutDown() { + nsCOMPtr<nsIThread> thread; + { + MutexAutoLock lock(mMutex); + if (mTimer) { + mTimer->Cancel(); + mTimer = nullptr; + } + thread = mThread.forget(); + } + // thread->Shutdown() spins a nested event loop while waiting for the thread + // to end. But the thread might execute some previously dispatched event that + // wants to lock our mutex, too, before arriving at the shutdown event. + if (thread) { + thread->Shutdown(); + } +} + +// We will use this to poll for low memory. +NS_IMETHODIMP +nsAvailableMemoryWatcher::Notify(nsITimer* aTimer) { + MutexAutoLock lock(mMutex); + if (!mThread) { + // If we've made it this far and there's no |mThread|, + // we might have failed to dispatch it for some reason. + MOZ_ASSERT(mThread); + return NS_ERROR_FAILURE; + } + nsresult rv = mThread->Dispatch( + NS_NewRunnableFunction("MemoryPoller", [self = RefPtr{this}]() { + if (self->IsMemoryLow()) { + self->HandleLowMemory(); + } else { + self->MaybeHandleHighMemory(); + } + })); + + if NS_FAILED (rv) { + NS_WARNING("Cannot dispatch memory polling event."); + } + return NS_OK; +} + +void nsAvailableMemoryWatcher::HandleLowMemory() { + MutexAutoLock lock(mMutex); + if (!mTimer) { + // We have been shut down from outside while in flight. + return; + } + if (!mUnderMemoryPressure) { + mUnderMemoryPressure = true; + UpdateCrashAnnotation(lock); + // Poll more frequently under memory pressure. + StartPolling(lock); + } + UpdateLowMemoryTimeStamp(); + // We handle low memory offthread, but we want to unload + // tabs only from the main thread, so we will dispatch this + // back to the main thread. + // Since we are doing this async, we don't need to unlock the mutex first; + // the AutoLock will unlock the mutex when we finish the dispatch. + NS_DispatchToMainThread(NS_NewRunnableFunction( + "nsAvailableMemoryWatcher::OnLowMemory", + [self = RefPtr{this}]() { self->mTabUnloader->UnloadTabAsync(); })); +} + +void nsAvailableMemoryWatcher::UpdateCrashAnnotation(const MutexAutoLock&) + MOZ_REQUIRES(mMutex) { + CrashReporter::AnnotateCrashReport( + CrashReporter::Annotation::LinuxUnderMemoryPressure, + mUnderMemoryPressure); +} + +// If memory is not low, we may need to dispatch an +// event for it if we have been under memory pressure. +// We can also adjust our polling interval. +void nsAvailableMemoryWatcher::MaybeHandleHighMemory() { + MutexAutoLock lock(mMutex); + if (!mTimer) { + // We have been shut down from outside while in flight. + return; + } + if (mUnderMemoryPressure) { + RecordTelemetryEventOnHighMemory(lock); + NS_NotifyOfEventualMemoryPressure(MemoryPressureState::NoPressure); + mUnderMemoryPressure = false; + UpdateCrashAnnotation(lock); + } + StartPolling(lock); +} + +// When we change the polling interval, we will need to restart the timer +// on the new interval. +void nsAvailableMemoryWatcher::StartPolling(const MutexAutoLock& aLock) + MOZ_REQUIRES(mMutex) { + uint32_t pollingInterval = mUnderMemoryPressure + ? kLowMemoryPollingIntervalMS + : kHighMemoryPollingIntervalMS; + if (!mPolling) { + // Restart the timer with the new interval if it has stopped. + // For testing, use a small polling interval. + if (NS_SUCCEEDED( + mTimer->InitWithCallback(this, gIsGtest ? 10 : pollingInterval, + nsITimer::TYPE_REPEATING_SLACK))) { + mPolling = true; + } + } else { + mTimer->SetDelay(gIsGtest ? 10 : pollingInterval); + } +} + +// Observe events for shutting down and starting/stopping the timer. +NS_IMETHODIMP +nsAvailableMemoryWatcher::Observe(nsISupports* aSubject, const char* aTopic, + const char16_t* aData) { + nsresult rv = nsAvailableMemoryWatcherBase::Observe(aSubject, aTopic, aData); + if (NS_FAILED(rv)) { + return rv; + } + + if (strcmp(aTopic, "xpcom-shutdown") == 0) { + ShutDown(); + } else { + MutexAutoLock lock(mMutex); + if (mTimer) { + if (strcmp(aTopic, "user-interaction-active") == 0) { + StartPolling(lock); + } else if (strcmp(aTopic, "user-interaction-inactive") == 0) { + StopPolling(lock); + } + } + } + + return NS_OK; +} + +NS_IMETHODIMP nsAvailableMemoryWatcher::GetName(nsACString& aName) { + aName.AssignLiteral("nsAvailableMemoryWatcher"); + return NS_OK; +} + +} // namespace mozilla |