Bug 771195 - Fire memory pressure events on Gonk. r=dhylands a=blocking-basecamp
authorJustin Lebar <justin.lebar@gmail.com>
Wed, 31 Oct 2012 13:29:14 -0400
changeset 116558 94375f9952d6410131bf9dbed0c175f5727d2554
parent 116557 1d407736951c3e040d06cbe2263d20a88e34ae26
child 116559 840c1b46ad0bbf1a0b3ab2a03a9a99901741462f
push id1708
push userakeybl@mozilla.com
push dateMon, 19 Nov 2012 21:10:21 +0000
treeherdermozilla-beta@27b14fe50103 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersdhylands, blocking-basecamp
bugs771195
milestone18.0a2
Bug 771195 - Fire memory pressure events on Gonk. r=dhylands a=blocking-basecamp * * * Bug 771195 - Follow-up: Fix debug build error on a CLOSED TREE. r=me
b2g/app/b2g.js
hal/gonk/GonkHal.cpp
widget/gonk/GonkMemoryPressureMonitoring.cpp
widget/gonk/GonkMemoryPressureMonitoring.h
widget/gonk/Makefile.in
widget/gonk/nsAppShell.cpp
--- a/b2g/app/b2g.js
+++ b/b2g/app/b2g.js
@@ -530,19 +530,28 @@ pref("javascript.options.mem.gc_allocati
 // Show/Hide scrollbars when active/inactive
 pref("ui.showHideScrollbars", 1);
 
 // Enable the ProcessPriorityManager, and give processes with no visible
 // documents a 1s grace period before they're eligible to be marked as
 // background.
 pref("dom.ipc.processPriorityManager.enabled", true);
 pref("dom.ipc.processPriorityManager.gracePeriodMS", 1000);
+
+// Kernel parameters for how processes are killed on low-memory.
+pref("gonk.systemMemoryPressureRecoveryPollMS", 5000);
 pref("hal.processPriorityManager.gonk.masterOomScoreAdjust", 0);
+pref("hal.processPriorityManager.gonk.masterKillUnderMB", 1);
 pref("hal.processPriorityManager.gonk.foregroundOomScoreAdjust", 67);
+pref("hal.processPriorityManager.gonk.foregroundKillUnderMB", 4);
 pref("hal.processPriorityManager.gonk.backgroundOomScoreAdjust", 400);
+pref("hal.processPriorityManager.gonk.backgroundKillUnderMB", 8);
+pref("hal.processPriorityManager.gonk.notifyLowMemUnderMB", 10);
+
+// Niceness values (i.e., CPU priorities) for B2G processes.
 pref("hal.processPriorityManager.gonk.masterNice", -1);
 pref("hal.processPriorityManager.gonk.foregroundNice", 0);
 pref("hal.processPriorityManager.gonk.backgroundNice", 10);
 
 #ifndef DEBUG
 // Enable pre-launching content processes for improved startup time
 // (hiding latency).
 pref("dom.ipc.processPrelaunch.enabled", true);
--- a/hal/gonk/GonkHal.cpp
+++ b/hal/gonk/GonkHal.cpp
@@ -18,16 +18,17 @@
 #include <errno.h>
 #include <fcntl.h>
 #include <linux/android_alarm.h>
 #include <math.h>
 #include <stdio.h>
 #include <sys/syscall.h>
 #include <sys/resource.h>
 #include <time.h>
+#include <asm/page.h>
 
 #include "android/log.h"
 #include "cutils/properties.h"
 #include "hardware/hardware.h"
 #include "hardware/lights.h"
 #include "hardware_legacy/uevent.h"
 #include "hardware_legacy/vibrator.h"
 #include "hardware_legacy/power.h"
@@ -921,37 +922,119 @@ SetAlarm(int32_t aSeconds, int32_t aNano
     HAL_LOG(("Unable to set alarm: %s.", strerror(errno)));
     return false;
   }
 
   return true;
 }
 
 static int
-oomAdjOfOomScoreAdj(int aOomScoreAdj)
+OomAdjOfOomScoreAdj(int aOomScoreAdj)
 {
   // Convert OOM adjustment from the domain of /proc/<pid>/oom_score_adj
-  // to thew domain of /proc/<pid>/oom_adj.
+  // to the domain of /proc/<pid>/oom_adj.
 
   int adj;
 
   if (aOomScoreAdj < 0) {
     adj = (OOM_DISABLE * aOomScoreAdj) / OOM_SCORE_ADJ_MIN;
   } else {
     adj = (OOM_ADJUST_MAX * aOomScoreAdj) / OOM_SCORE_ADJ_MAX;
   }
 
   return adj;
 }
 
+static void
+EnsureKernelLowMemKillerParamsSet()
+{
+  static bool kernelLowMemKillerParamsSet;
+  if (kernelLowMemKillerParamsSet) {
+    return;
+  }
+  kernelLowMemKillerParamsSet = true;
+
+  HAL_LOG(("Setting kernel's low-mem killer parameters."));
+
+  // Set /sys/module/lowmemorykiller/parameters/{adj,minfree,notify_trigger}
+  // according to our prefs.  These files let us tune when the kernel kills
+  // processes when we're low on memory, and when it notifies us that we're
+  // running low on available memory.
+  //
+  // adj and minfree are both comma-separated lists of integers.  If adj="A,B"
+  // and minfree="X,Y", then the kernel will kill processes with oom_adj
+  // A or higher once we have fewer than X pages of memory free, and will kill
+  // processes with oom_adj B or higher once we have fewer than Y pages of
+  // memory free.
+  //
+  // notify_trigger is a single integer.   If we set notify_trigger=Z, then
+  // we'll get notified when there are fewer than Z pages of memory free.  (See
+  // GonkMemoryPressureMonitoring.cpp.)
+
+  // Build the adj and minfree strings.
+  nsAutoCString adjParams;
+  nsAutoCString minfreeParams;
+
+  const char* priorityClasses[] = {"master", "foreground", "background"};
+  for (size_t i = 0; i < NS_ARRAY_LENGTH(priorityClasses); i++) {
+    int32_t oomScoreAdj;
+    if (!NS_SUCCEEDED(Preferences::GetInt(nsPrintfCString(
+          "hal.processPriorityManager.gonk.%sOomScoreAdjust",
+          priorityClasses[i]).get(), &oomScoreAdj))) {
+      continue;
+    }
+
+    int32_t killUnderMB;
+    if (!NS_SUCCEEDED(Preferences::GetInt(nsPrintfCString(
+          "hal.processPriorityManager.gonk.%sKillUnderMB",
+          priorityClasses[i]).get(), &killUnderMB))) {
+      continue;
+    }
+
+    // adj is in oom_adj units.
+    adjParams.AppendPrintf("%d,", OomAdjOfOomScoreAdj(oomScoreAdj));
+
+    // minfree is in pages.
+    minfreeParams.AppendPrintf("%d,", killUnderMB * 1024 * 1024 / PAGE_SIZE);
+  }
+
+  // Strip off trailing commas.
+  adjParams.Cut(adjParams.Length() - 1, 1);
+  minfreeParams.Cut(minfreeParams.Length() - 1, 1);
+  if (!adjParams.IsEmpty() && !minfreeParams.IsEmpty()) {
+    WriteToFile("/sys/module/lowmemorykiller/parameters/adj", adjParams.get());
+    WriteToFile("/sys/module/lowmemorykiller/parameters/minfree", minfreeParams.get());
+  }
+
+  // Set the low-memory-notification threshold.
+  int32_t lowMemNotifyThresholdMB;
+  if (NS_SUCCEEDED(Preferences::GetInt(
+        "hal.processPriorityManager.gonk.notifyLowMemUnderMB",
+        &lowMemNotifyThresholdMB))) {
+
+    // notify_trigger is in pages.
+    WriteToFile("/sys/module/lowmemorykiller/parameters/notify_trigger",
+      nsPrintfCString("%d", lowMemNotifyThresholdMB * 1024 * 1024 / PAGE_SIZE).get());
+  }
+}
+
 void
 SetProcessPriority(int aPid, ProcessPriority aPriority)
 {
   HAL_LOG(("SetProcessPriority(pid=%d, priority=%d)", aPid, aPriority));
 
+  // If this is the first time SetProcessPriority was called, set the kernel's
+  // OOM parameters according to our prefs.
+  //
+  // We could/should do this on startup instead of waiting for the first
+  // SetProcessPriorityCall.  But in practice, the master process needs to set
+  // its priority early in the game, so we can reasonably rely on
+  // SetProcessPriority being called early in startup.
+  EnsureKernelLowMemKillerParamsSet();
+
   const char* priorityStr = NULL;
   switch (aPriority) {
   case PROCESS_PRIORITY_BACKGROUND:
     priorityStr = "background";
     break;
   case PROCESS_PRIORITY_FOREGROUND:
     priorityStr = "foreground";
     break;
@@ -983,17 +1066,17 @@ SetProcessPriority(int aPid, ProcessPrio
     }
 
     // We try the newer interface first, and fall back to the older interface
     // on failure.
 
     if (!WriteToFile(nsPrintfCString("/proc/%d/oom_score_adj", aPid).get(),
                      nsPrintfCString("%d", clampedOomScoreAdj).get()))
     {
-      int oomAdj = oomAdjOfOomScoreAdj(clampedOomScoreAdj);
+      int oomAdj = OomAdjOfOomScoreAdj(clampedOomScoreAdj);
 
       WriteToFile(nsPrintfCString("/proc/%d/oom_adj", aPid).get(),
                   nsPrintfCString("%d", oomAdj).get());
     }
   }
 
   int32_t nice = 0;
   rv = Preferences::GetInt(nsPrintfCString(
new file mode 100644
--- /dev/null
+++ b/widget/gonk/GonkMemoryPressureMonitoring.cpp
@@ -0,0 +1,267 @@
+/* -*- 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 "GonkMemoryPressureMonitoring.h"
+#include "mozilla/FileUtils.h"
+#include "mozilla/Monitor.h"
+#include "mozilla/Preferences.h"
+#include "mozilla/Services.h"
+#include "nsIObserver.h"
+#include "nsIObserverService.h"
+#include "nsThreadUtils.h"
+#include <errno.h>
+#include <fcntl.h>
+#include <poll.h>
+#include <android/log.h>
+
+#define LOG(args...)  \
+  __android_log_print(ANDROID_LOG_INFO, "GonkMemoryPressure" , ## args)
+
+using namespace mozilla;
+
+namespace {
+
+class MemoryPressureRunnable : public nsRunnable
+{
+public:
+  NS_IMETHOD Run()
+  {
+    MOZ_ASSERT(NS_IsMainThread());
+    LOG("Dispatching low-memory memory-pressure event");
+
+    nsCOMPtr<nsIObserverService> os = services::GetObserverService();
+    if (os) {
+      os->NotifyObservers(nullptr, "memory-pressure",
+                          NS_LITERAL_STRING("low-memory").get());
+    }
+    return NS_OK;
+  }
+};
+
+/**
+ * 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
+  : public nsIRunnable
+  , public nsIObserver
+{
+public:
+  MemoryPressureWatcher()
+    : mMonitor("MemoryPressureWatcher")
+    , mShuttingDown(false)
+  {
+  }
+
+  NS_DECL_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, /* holdsWeak */ false);
+
+    // 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.
+    mPollMS = Preferences::GetUint("gonk.systemMemoryPressureRecoveryPollMS",
+                                   /* default */ 5000);
+
+    int pipes[2];
+    NS_ENSURE_STATE(!pipe(pipes));
+    mShutdownPipeRead = pipes[0];
+    mShutdownPipeWrite = pipes[1];
+    return NS_OK;
+  }
+
+  NS_IMETHOD Observe(nsISupports* aSubject, const char* aTopic,
+                     const PRUnichar* 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()
+  {
+    MOZ_ASSERT(!NS_IsMainThread());
+
+    int lowMemFd = open("/sys/kernel/mm/lowmemkiller/notify_trigger_active",
+                        O_RDONLY | O_CLOEXEC);
+    NS_ENSURE_STATE(lowMemFd != -1);
+    ScopedClose autoClose(lowMemFd);
+
+    nsresult rv = CheckForMemoryPressure(lowMemFd, nullptr);
+    NS_ENSURE_SUCCESS(rv, rv);
+
+    while (true) {
+      // Wait for a notification on lowMemFd or for data to be written to
+      // mShutdownPipeWrite.  (poll(lowMemFd, POLLPRI) blocks until we're under
+      // memory pressure.)
+      struct pollfd pollfds[2];
+      pollfds[0].fd = lowMemFd;
+      pollfds[0].events = POLLPRI;
+      pollfds[1].fd = mShutdownPipeRead;
+      pollfds[1].events = POLLIN;
+
+      int pollRv;
+      do {
+        pollRv = poll(pollfds, NS_ARRAY_LENGTH(pollfds), /* timeout */ -1);
+      } while (pollRv == -1 && errno == EINTR);
+
+      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 lowMemFd 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().
+
+      nsRefPtr<MemoryPressureRunnable> memoryPressureRunnable =
+        new MemoryPressureRunnable();
+      NS_DispatchToMainThread(memoryPressureRunnable);
+
+      // Manually check lowMemFd until we observe that memory pressure is over.
+      // We won't fire any more memory-pressure events until we observe that
+      // we're no longer under pressure.
+      bool memoryPressure;
+      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(lowMemFd, &memoryPressure);
+        NS_ENSURE_SUCCESS(rv, rv);
+      } while (memoryPressure);
+
+      LOG("Memory pressure is over.");
+    }
+
+    return NS_OK;
+  }
+
+private:
+  /**
+   * Read from aLowMemFd, which we assume corresponds to the
+   * notify_trigger_active sysfs node, and determine whether we're currently
+   * under memory pressure.
+   *
+   * We don't expect this method to block.
+   */
+  nsresult CheckForMemoryPressure(int aLowMemFd, bool* aOut)
+  {
+    if (aOut) {
+      *aOut = false;
+    }
+
+    lseek(aLowMemFd, 0, SEEK_SET);
+
+    char buf[2];
+    int nread;
+    do {
+      nread = read(aLowMemFd, buf, sizeof(buf));
+    } while(nread == -1 && errno == EINTR);
+    NS_ENSURE_STATE(nread == 2);
+
+    // The notify_trigger_active sysfs node should contain either "0\n" or
+    // "1\n".  The latter indicates memory pressure.
+    if (aOut) {
+      *aOut = buf[0] == '1' && buf[1] == '\n';
+    }
+    return NS_OK;
+  }
+
+  Monitor mMonitor;
+  uint32_t mPollMS;
+  bool mShuttingDown;
+
+  ScopedClose mShutdownPipeRead;
+  ScopedClose mShutdownPipeWrite;
+};
+
+NS_IMPL_THREADSAFE_ISUPPORTS2(MemoryPressureWatcher, nsIRunnable, nsIObserver);
+
+} // anonymous namespace
+
+namespace mozilla {
+
+void
+InitGonkMemoryPressureMonitoring()
+{
+  // memoryPressureWatcher is held alive by the observer service.
+  nsRefPtr<MemoryPressureWatcher> memoryPressureWatcher =
+    new MemoryPressureWatcher();
+  NS_ENSURE_SUCCESS_VOID(memoryPressureWatcher->Init());
+
+  nsCOMPtr<nsIThread> thread;
+  NS_NewThread(getter_AddRefs(thread), memoryPressureWatcher);
+}
+
+} // namespace mozilla
new file mode 100644
--- /dev/null
+++ b/widget/gonk/GonkMemoryPressureMonitoring.h
@@ -0,0 +1,14 @@
+/* -*- 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/. */
+
+#ifndef mozilla_GonkMemoryPressureMonitoring_h_
+#define mozilla_GonkMemoryPressureMonitoring_h_
+
+namespace mozilla {
+void InitGonkMemoryPressureMonitoring();
+}
+
+#endif /* mozilla_GonkMemoryPressureMonitoring_h_ */
--- a/widget/gonk/Makefile.in
+++ b/widget/gonk/Makefile.in
@@ -37,16 +37,17 @@ CPPSRCS	= \
 	HWComposer.cpp \
 	nsAppShell.cpp \
 	nsWidgetFactory.cpp \
 	nsWindow.cpp \
 	nsLookAndFeel.cpp \
 	nsIdleServiceGonk.cpp \
 	OrientationObserver.cpp \
 	EventHub.cpp \
+	GonkMemoryPressureMonitoring.cpp \
 	Input.cpp \
 	InputApplication.cpp \
 	InputDispatcher.cpp \
 	InputListener.cpp \
 	InputReader.cpp \
 	InputTransport.cpp \
 	InputWindow.cpp \
 	Keyboard.cpp \
--- a/widget/gonk/nsAppShell.cpp
+++ b/widget/gonk/nsAppShell.cpp
@@ -38,16 +38,17 @@
 #include "nsDOMTouchEvent.h"
 #include "nsGkAtoms.h"
 #include "nsGUIEvent.h"
 #include "nsIObserverService.h"
 #include "nsIScreen.h"
 #include "nsScreenManagerGonk.h"
 #include "nsWindow.h"
 #include "OrientationObserver.h"
+#include "GonkMemoryPressureMonitoring.h"
 
 #include "android/log.h"
 #include "libui/EventHub.h"
 #include "libui/InputReader.h"
 #include "libui/InputDispatcher.h"
 
 #include "sampler.h"
 
@@ -612,16 +613,18 @@ nsAppShell::Init()
     NS_ENSURE_TRUE(epollfd >= 0, NS_ERROR_UNEXPECTED);
 
     int ret = pipe2(signalfds, O_NONBLOCK);
     NS_ENSURE_FALSE(ret, NS_ERROR_UNEXPECTED);
 
     rv = AddFdHandler(signalfds[0], pipeHandler, "");
     NS_ENSURE_SUCCESS(rv, rv);
 
+    InitGonkMemoryPressureMonitoring();
+
     // Delay initializing input devices until the screen has been
     // initialized (and we know the resolution).
     return rv;
 }
 
 NS_IMETHODIMP
 nsAppShell::Exit()
 {