diff options
Diffstat (limited to 'widget/gonk/GonkMemoryPressureMonitoring.cpp')
-rw-r--r-- | widget/gonk/GonkMemoryPressureMonitoring.cpp | 359 |
1 files changed, 359 insertions, 0 deletions
diff --git a/widget/gonk/GonkMemoryPressureMonitoring.cpp b/widget/gonk/GonkMemoryPressureMonitoring.cpp new file mode 100644 index 000000000..0fafb37cf --- /dev/null +++ b/widget/gonk/GonkMemoryPressureMonitoring.cpp @@ -0,0 +1,359 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=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 <android/log.h> +#include <errno.h> +#include <fcntl.h> +#include <poll.h> +#include <sys/sysinfo.h> + +#include "GonkMemoryPressureMonitoring.h" +#include "mozilla/ArrayUtils.h" +#include "mozilla/FileUtils.h" +#include "mozilla/Monitor.h" +#include "mozilla/Preferences.h" +#include "mozilla/ProcessPriorityManager.h" +#include "mozilla/Services.h" +#include "nsIObserver.h" +#include "nsIObserverService.h" +#include "nsMemoryPressure.h" +#include "nsPrintfCString.h" +#include "nsThreadUtils.h" + +#define LOG(args...) \ + __android_log_print(ANDROID_LOG_INFO, "GonkMemoryPressure" , ## args) + +using namespace mozilla; + +namespace { + +/** + * MemoryPressureWatcher watches sysfs from its own thread to notice when the + * system is under memory pressure. When we observe memory pressure, we use + * MemoryPressureRunnable to notify observers that they should release memory. + * + * When the system is under memory pressure, we don't want to constantly fire + * memory-pressure events. So instead, we try to detect when sysfs indicates + * that we're no longer under memory pressure, and only then start firing events + * again. + * + * (This is a bit problematic because we can't poll() to detect when we're no + * longer under memory pressure; instead we have to periodically read the sysfs + * node. If we remain under memory pressure for a long time, this means we'll + * continue waking up to read from the node for a long time, potentially wasting + * battery life. Hopefully we don't hit this case in practice! We write to + * logcat each time we go around this loop so it's at least noticable.) + * + * Shutting down safely is a bit of a chore. XPCOM won't shut down until all + * threads exit, so we need to exit the Run() method below on shutdown. But our + * thread might be blocked in one of two situations: We might be poll()'ing the + * sysfs node waiting for memory pressure to occur, or we might be asleep + * waiting to read() the sysfs node to see if we're no longer under memory + * pressure. + * + * To let us wake up from the poll(), we poll() not just the sysfs node but also + * a pipe, which we write to on shutdown. To let us wake up from sleeping + * between read()s, we sleep by Wait()'ing on a monitor, which we notify on + * shutdown. + */ +class MemoryPressureWatcher final + : public nsIRunnable + , public nsIObserver +{ +public: + MemoryPressureWatcher() + : mMonitor("MemoryPressureWatcher") + , mLowMemTriggerKB(0) + , mPageSize(0) + , mShuttingDown(false) + , mTriggerFd(-1) + , mShutdownPipeRead(-1) + , mShutdownPipeWrite(-1) + { + } + + NS_DECL_THREADSAFE_ISUPPORTS + + nsresult Init() + { + nsCOMPtr<nsIObserverService> os = services::GetObserverService(); + NS_ENSURE_STATE(os); + + // The observer service holds us alive. + os->AddObserver(this, NS_XPCOM_SHUTDOWN_OBSERVER_ID, /* ownsWeak */ false); + + // Initialize the internal state + mPageSize = sysconf(_SC_PAGESIZE); + ReadPrefs(); + nsresult rv = OpenFiles(); + NS_ENSURE_SUCCESS(rv, rv); + SetLowMemTrigger(mSoftLowMemTriggerKB); + + return NS_OK; + } + + NS_IMETHOD Observe(nsISupports* aSubject, const char* aTopic, + const char16_t* aData) + { + MOZ_ASSERT(strcmp(aTopic, NS_XPCOM_SHUTDOWN_OBSERVER_ID) == 0); + LOG("Observed XPCOM shutdown."); + + MonitorAutoLock lock(mMonitor); + mShuttingDown = true; + mMonitor.Notify(); + + int rv; + do { + // Write something to the pipe; doesn't matter what. + uint32_t dummy = 0; + rv = write(mShutdownPipeWrite, &dummy, sizeof(dummy)); + } while(rv == -1 && errno == EINTR); + + return NS_OK; + } + + NS_IMETHOD Run() override + { + MOZ_ASSERT(!NS_IsMainThread()); + + int triggerResetTimeout = -1; + bool memoryPressure; + nsresult rv = CheckForMemoryPressure(&memoryPressure); + NS_ENSURE_SUCCESS(rv, rv); + + while (true) { + // Wait for a notification on mTriggerFd or for data to be written to + // mShutdownPipeWrite. (poll(mTriggerFd, POLLPRI) blocks until we're + // under memory pressure or until we time out, the time out is used + // to adjust the trigger level after a memory pressure event.) + struct pollfd pollfds[2]; + pollfds[0].fd = mTriggerFd; + pollfds[0].events = POLLPRI; + pollfds[1].fd = mShutdownPipeRead; + pollfds[1].events = POLLIN; + + int pollRv = MOZ_TEMP_FAILURE_RETRY( + poll(pollfds, ArrayLength(pollfds), triggerResetTimeout) + ); + + if (pollRv == 0) { + // Timed out, adjust the trigger and update the timeout. + triggerResetTimeout = AdjustTrigger(triggerResetTimeout); + continue; + } + + if (pollfds[1].revents) { + // Something was written to our shutdown pipe; we're outta here. + LOG("shutting down (1)"); + return NS_OK; + } + + // If pollfds[1] isn't happening, pollfds[0] ought to be! + if (!(pollfds[0].revents & POLLPRI)) { + LOG("Unexpected revents value after poll(): %d. " + "Shutting down GonkMemoryPressureMonitoring.", pollfds[0].revents); + return NS_ERROR_FAILURE; + } + + // POLLPRI on mTriggerFd indicates that we're in a low-memory situation. + // We could read lowMemFd to double-check, but we've observed that the + // read sometimes completes after the memory-pressure event is over, so + // let's just believe the result of poll(). + rv = DispatchMemoryPressure(MemPressure_New); + NS_ENSURE_SUCCESS(rv, rv); + + // Move to the hard level if we're on the soft one. + if (mLowMemTriggerKB > mHardLowMemTriggerKB) { + SetLowMemTrigger(mHardLowMemTriggerKB); + } + + // Manually check mTriggerFd until we observe that memory pressure is + // over. We won't fire any more low-memory events until we observe that + // we're no longer under pressure. Instead, we fire low-memory-ongoing + // events, which cause processes to keep flushing caches but will not + // trigger expensive GCs and other attempts to save memory that are + // likely futile at this point. + do { + { + MonitorAutoLock lock(mMonitor); + + // We need to check mShuttingDown before we wait here, in order to + // catch a shutdown signal sent after we poll()'ed mShutdownPipeRead + // above but before we started waiting on the monitor. But we don't + // need to check after we wait, because we'll either do another + // iteration of this inner loop, in which case we'll check + // mShuttingDown, or we'll exit this loop and do another iteration + // of the outer loop, in which case we'll check the shutdown pipe. + if (mShuttingDown) { + LOG("shutting down (2)"); + return NS_OK; + } + mMonitor.Wait(PR_MillisecondsToInterval(mPollMS)); + } + + LOG("Checking to see if memory pressure is over."); + rv = CheckForMemoryPressure(&memoryPressure); + NS_ENSURE_SUCCESS(rv, rv); + + if (memoryPressure) { + rv = DispatchMemoryPressure(MemPressure_Ongoing); + NS_ENSURE_SUCCESS(rv, rv); + continue; + } + } while (false); + + if (XRE_IsParentProcess()) { + // The main process will try to adjust the trigger. + triggerResetTimeout = mPollMS * 2; + } + + LOG("Memory pressure is over."); + } + + return NS_OK; + } + +protected: + ~MemoryPressureWatcher() {} + +private: + void ReadPrefs() { + // While we're under memory pressure, we periodically read() + // notify_trigger_active to try and see when we're no longer under memory + // pressure. mPollMS indicates how many milliseconds we wait between those + // read()s. + Preferences::AddUintVarCache(&mPollMS, + "gonk.systemMemoryPressureRecoveryPollMS", /* default */ 5000); + + // We have two values for the notify trigger, a soft one which is triggered + // before we start killing background applications and an hard one which is + // after we've killed background applications but before we start killing + // foreground ones. + Preferences::AddUintVarCache(&mSoftLowMemTriggerKB, + "gonk.notifySoftLowMemUnderKB", /* default */ 43008); + Preferences::AddUintVarCache(&mHardLowMemTriggerKB, + "gonk.notifyHardLowMemUnderKB", /* default */ 14336); + } + + nsresult OpenFiles() { + mTriggerFd = open("/sys/kernel/mm/lowmemkiller/notify_trigger_active", + O_RDONLY | O_CLOEXEC); + NS_ENSURE_STATE(mTriggerFd != -1); + + int pipes[2]; + NS_ENSURE_STATE(!pipe(pipes)); + mShutdownPipeRead = pipes[0]; + mShutdownPipeWrite = pipes[1]; + return NS_OK; + } + + /** + * Set the low memory trigger to the specified value, this can be done by + * the main process alone. + */ + void SetLowMemTrigger(uint32_t aValue) { + if (XRE_IsParentProcess()) { + nsPrintfCString str("%ld", (aValue * 1024) / mPageSize); + if (WriteSysFile("/sys/module/lowmemorykiller/parameters/notify_trigger", + str.get())) { + mLowMemTriggerKB = aValue; + } + } + } + + /** + * Read from the trigger file descriptor and determine whether we're + * currently under memory pressure. + * + * We don't expect this method to block. + */ + nsresult CheckForMemoryPressure(bool* aOut) + { + *aOut = false; + + lseek(mTriggerFd, 0, SEEK_SET); + + char buf[2]; + int nread = MOZ_TEMP_FAILURE_RETRY(read(mTriggerFd, buf, sizeof(buf))); + NS_ENSURE_STATE(nread == 2); + + // The notify_trigger_active sysfs node should contain either "0\n" or + // "1\n". The latter indicates memory pressure. + *aOut = (buf[0] == '1'); + return NS_OK; + } + + int AdjustTrigger(int timeout) + { + if (!XRE_IsParentProcess()) { + return -1; // Only the main process can adjust the trigger. + } + + struct sysinfo info; + int rv = sysinfo(&info); + if (rv < 0) { + return -1; // Without system information we're blind, bail out. + } + + size_t freeMemory = (info.freeram * info.mem_unit) / 1024; + + if (freeMemory > mSoftLowMemTriggerKB) { + SetLowMemTrigger(mSoftLowMemTriggerKB); + return -1; // Trigger adjusted, wait indefinitely. + } + + // Wait again but double the duration, max once per day. + return std::min(86400000, timeout * 2); + } + + /** + * Dispatch the specified memory pressure event unless a high-priority + * process is present. If a high-priority process is present then it's likely + * responding to an urgent event (an incoming call or message for example) so + * avoid wasting CPU time responding to low-memory events. + */ + nsresult DispatchMemoryPressure(MemoryPressureState state) + { + if (ProcessPriorityManager::AnyProcessHasHighPriority()) { + return NS_OK; + } + + return NS_DispatchMemoryPressure(state); + } + + Monitor mMonitor; + uint32_t mPollMS; // Ongoing pressure poll delay + uint32_t mSoftLowMemTriggerKB; // Soft memory pressure level + uint32_t mHardLowMemTriggerKB; // Hard memory pressure level + uint32_t mLowMemTriggerKB; // Current value of the trigger + size_t mPageSize; + bool mShuttingDown; + + ScopedClose mTriggerFd; + ScopedClose mShutdownPipeRead; + ScopedClose mShutdownPipeWrite; +}; + +NS_IMPL_ISUPPORTS(MemoryPressureWatcher, nsIRunnable, nsIObserver); + +} // namespace + +namespace mozilla { + +void +InitGonkMemoryPressureMonitoring() +{ + // memoryPressureWatcher is held alive by the observer service. + RefPtr<MemoryPressureWatcher> memoryPressureWatcher = + new MemoryPressureWatcher(); + NS_ENSURE_SUCCESS_VOID(memoryPressureWatcher->Init()); + + nsCOMPtr<nsIThread> thread; + NS_NewNamedThread("MemoryPressure", getter_AddRefs(thread), + memoryPressureWatcher); +} + +} // namespace mozilla |