Bug 1451005 - Introduce a timer-based poller for detecting low-memory scenarios; r?njn draft
authorGabriele Svelto <gsvelto@mozilla.com>
Thu, 17 May 2018 14:48:02 +0200
changeset 796275 61fbd1603f57f8a42b25d379c78643eb848e9cb8
parent 794690 45ec8fd380dd2c308e79dbb396ca87f2ce9b3f9c
child 796276 60f1fd814f46bb98da7a5d73e648aeac837fc8b7
push id110202
push usergsvelto@mozilla.com
push dateThu, 17 May 2018 12:48:51 +0000
reviewersnjn
bugs1451005
milestone62.0a1
Bug 1451005 - Introduce a timer-based poller for detecting low-memory scenarios; r?njn This patch introduces a new polling mechanism to detect low-memory scenarios. The timer fires at a relatively slow pace and stops whenever the user stops interacting with Firefox to avoid consuming power needlessly. The polling rate is up to 3 orders of magnitude slower than the current tracker and is throttled when memory is running low. It also doesn't suffer from data races that were possible with existing tracker. Contrary to the old available memory tracker which relied on a Windows-specific mechanism, this one could be made to work on other platforms too. The current implementation only supports Windows 64-bit builds though. MozReview-Commit-ID: CFHuTDqjPbL
xpcom/base/AvailableMemoryTracker.cpp
--- a/xpcom/base/AvailableMemoryTracker.cpp
+++ b/xpcom/base/AvailableMemoryTracker.cpp
@@ -12,39 +12,46 @@
 #include "nsIMemoryReporter.h"
 #include "nsMemoryPressure.h"
 #endif
 
 #include "nsIObserver.h"
 #include "nsIObserverService.h"
 #include "nsIRunnable.h"
 #include "nsISupports.h"
+#include "nsITimer.h"
 #include "nsThreadUtils.h"
+#include "nsXULAppAPI.h"
 
-#include "mozilla/Preferences.h"
+#include "mozilla/ResultExtensions.h"
 #include "mozilla/Services.h"
+#include "mozilla/Unused.h"
 
 #if defined(XP_WIN)
 #   include "nsWindowsDllInterceptor.h"
 #   include <windows.h>
 #endif
 
 #if defined(MOZ_MEMORY)
 #   include "mozmemory.h"
 #endif  // MOZ_MEMORY
 
 using namespace mozilla;
 
 namespace {
 
-#if defined(_M_IX86) && defined(XP_WIN)
+#if defined(XP_WIN)
 
 // Fire a low-memory notification if we have less than this many bytes of
 // virtual address space available.
+#if defined(HAVE_64BIT_BUILD)
+static const size_t kLowVirtualMemoryThreshold = 0;
+#else
 static const size_t kLowVirtualMemoryThreshold = 256 * 1024 * 1024;
+#endif
 
 // Fire a low-memory notification if we have less than this many bytes of commit
 // space (physical memory plus page file) left.
 static const size_t kLowCommitSpaceThreshold = 256 * 1024 * 1024;
 
 // Fire a low-memory notification if we have less than this many bytes of
 // physical memory available on the whole machine.
 static const size_t kLowPhysicalMemoryThreshold = 0;
@@ -52,16 +59,18 @@ static const size_t kLowPhysicalMemoryTh
 // Don't fire a low-memory notification because of low available physical
 // memory or low commit space more often than this interval.
 static const uint32_t kLowMemoryNotificationIntervalMS = 10000;
 
 Atomic<uint32_t, MemoryOrdering::Relaxed> sNumLowVirtualMemEvents;
 Atomic<uint32_t, MemoryOrdering::Relaxed> sNumLowCommitSpaceEvents;
 Atomic<uint32_t, MemoryOrdering::Relaxed> sNumLowPhysicalMemEvents;
 
+#if !defined(HAVE_64BIT_BUILD)
+
 WindowsDllInterceptor sKernel32Intercept;
 WindowsDllInterceptor sGdi32Intercept;
 
 // Has Init() been called?
 bool sInitialized = false;
 
 // Has Activate() been called?  The hooks don't do anything until this happens.
 bool sHooksActive = false;
@@ -248,16 +257,199 @@ CreateDIBSectionHook(HDC aDC,
 
   if (doCheck) {
     CheckMemAvailable();
   }
 
   return result;
 }
 
+#else
+
+class nsAvailableMemoryWatcher final : public nsIObserver,
+                                       public nsITimerCallback
+{
+public:
+  NS_DECL_ISUPPORTS
+  NS_DECL_NSIOBSERVER
+  NS_DECL_NSITIMERCALLBACK
+
+  nsresult Init();
+
+private:
+  // Poll the amount of free memory at this rate.
+  static const uint32_t kPollingIntervalMS = 1000;
+
+  // Observer topics we subscribe to
+  static const char* const kObserverTopics[];
+
+  static bool IsVirtualMemoryLow(const MEMORYSTATUSEX& aStat);
+  static bool IsCommitSpaceLow(const MEMORYSTATUSEX& aStat);
+  static bool IsPhysicalMemoryLow(const MEMORYSTATUSEX& aStat);
+
+  ~nsAvailableMemoryWatcher() {};
+  void AdjustPollingInterval(const bool aLowMemory);
+  void SendMemoryPressureEvent();
+  void Shutdown();
+
+  nsCOMPtr<nsITimer> mTimer;
+  bool mUnderMemoryPressure;
+};
+
+const char* const nsAvailableMemoryWatcher::kObserverTopics[] = {
+  "quit-application",
+  "user-interaction-active",
+  "user-interaction-inactive",
+};
+
+NS_IMPL_ISUPPORTS(nsAvailableMemoryWatcher, nsIObserver, nsITimerCallback)
+
+nsresult
+nsAvailableMemoryWatcher::Init()
+{
+  mTimer = NS_NewTimer();
+
+  nsCOMPtr<nsIObserverService> observerService = services::GetObserverService();
+  MOZ_ASSERT(observerService);
+
+  for (auto topic : kObserverTopics) {
+    nsresult rv = observerService->AddObserver(this, topic,
+                                               /* ownsWeak */ false);
+    NS_ENSURE_SUCCESS(rv, rv);
+  }
+
+  MOZ_TRY(mTimer->InitWithCallback(this, kPollingIntervalMS,
+                                   nsITimer::TYPE_REPEATING_SLACK));
+  return NS_OK;
+}
+
+void
+nsAvailableMemoryWatcher::Shutdown()
+{
+  nsCOMPtr<nsIObserverService> observerService = services::GetObserverService();
+  MOZ_ASSERT(observerService);
+
+  for (auto topic : kObserverTopics) {
+    Unused << observerService->RemoveObserver(this, topic);
+  }
+
+  if (mTimer) {
+    mTimer->Cancel();
+    mTimer = nullptr;
+  }
+}
+
+/* static */ bool
+nsAvailableMemoryWatcher::IsVirtualMemoryLow(const MEMORYSTATUSEX& aStat)
+{
+  if ((kLowVirtualMemoryThreshold != 0) &&
+      (aStat.ullAvailVirtual < kLowVirtualMemoryThreshold)) {
+    sNumLowVirtualMemEvents++;
+    return true;
+  }
+
+  return false;
+}
+
+/* static */ bool
+nsAvailableMemoryWatcher::IsCommitSpaceLow(const MEMORYSTATUSEX& aStat)
+{
+  if ((kLowCommitSpaceThreshold != 0) &&
+      (aStat.ullAvailPageFile < kLowCommitSpaceThreshold)) {
+    sNumLowCommitSpaceEvents++;
+    return true;
+  }
+
+  return false;
+}
+
+/* static */ bool
+nsAvailableMemoryWatcher::IsPhysicalMemoryLow(const MEMORYSTATUSEX& aStat)
+{
+  if ((kLowPhysicalMemoryThreshold != 0) &&
+      (aStat.ullAvailPhys < kLowPhysicalMemoryThreshold)) {
+    sNumLowPhysicalMemEvents++;
+    return true;
+  }
+
+  return false;
+}
+
+void
+nsAvailableMemoryWatcher::SendMemoryPressureEvent()
+{
+    MemoryPressureState state = mUnderMemoryPressure ? MemPressure_Ongoing
+                                                     : MemPressure_New;
+    NS_DispatchEventualMemoryPressure(state);
+}
+
+void
+nsAvailableMemoryWatcher::AdjustPollingInterval(const bool aLowMemory)
+{
+  if (aLowMemory) {
+    // We entered a low-memory state, wait for a longer interval before polling
+    // again as there's no point in rapidly sending further notifications.
+    mTimer->SetDelay(kLowMemoryNotificationIntervalMS);
+  } else if (mUnderMemoryPressure) {
+    // We were under memory pressure but we're not anymore, resume polling at
+    // a faster pace.
+    mTimer->SetDelay(kPollingIntervalMS);
+  }
+}
+
+// Timer callback, polls memory stats to detect low-memory conditions. This
+// will send memory-pressure events if memory is running low and adjust the
+// polling interval accordingly.
+NS_IMETHODIMP
+nsAvailableMemoryWatcher::Notify(nsITimer* aTimer)
+{
+  MEMORYSTATUSEX stat;
+  stat.dwLength = sizeof(stat);
+  bool success = GlobalMemoryStatusEx(&stat);
+
+  if (success) {
+    bool lowMemory =
+      IsVirtualMemoryLow(stat) ||
+      IsCommitSpaceLow(stat) ||
+      IsPhysicalMemoryLow(stat);
+
+    if (lowMemory) {
+      SendMemoryPressureEvent();
+    }
+
+    AdjustPollingInterval(lowMemory);
+    mUnderMemoryPressure = lowMemory;
+  }
+
+  return NS_OK;
+}
+
+// Observer service callback, used to stop the polling timer when the user
+// stops interacting with Firefox and resuming it when they interact again.
+// Also used to shut down the service if the application is quitting.
+NS_IMETHODIMP
+nsAvailableMemoryWatcher::Observe(nsISupports* aSubject, const char* aTopic,
+                                  const char16_t* aData)
+{
+  if (strcmp(aTopic, "quit-application") == 0) {
+    Shutdown();
+  } else if (strcmp(aTopic, "user-interaction-inactive") == 0) {
+    mTimer->Cancel();
+  } else if (strcmp(aTopic, "user-interaction-active") == 0) {
+    mTimer->InitWithCallback(this, kPollingIntervalMS,
+                             nsITimer::TYPE_REPEATING_SLACK);
+  } else {
+    MOZ_ASSERT_UNREACHABLE("Unknown topic");
+  }
+
+  return NS_OK;
+}
+
+#endif // !defined(HAVE_64BIT_BUILD)
+
 static int64_t
 LowMemoryEventsVirtualDistinguishedAmount()
 {
   return sNumLowVirtualMemEvents;
 }
 
 static int64_t
 LowMemoryEventsPhysicalDistinguishedAmount()
@@ -302,17 +494,17 @@ public:
 "machine will start to page if it runs out of physical memory.  This may "
 "cause it to run slowly, but it shouldn't cause it to crash.");
 
     return NS_OK;
   }
 };
 NS_IMPL_ISUPPORTS(LowEventsReporter, nsIMemoryReporter)
 
-#endif // defined(_M_IX86) && defined(XP_WIN)
+#endif // defined(XP_WIN)
 
 /**
  * This runnable is executed in response to a memory-pressure event; we spin
  * the event-loop when receiving the memory-pressure event in the hope that
  * other observers will synchronously free some memory that we'll be able to
  * purge here.
  */
 class nsJemallocFreeDirtyPagesRunnable final : public nsIRunnable
@@ -391,46 +583,56 @@ nsMemoryPressureWatcher::Observe(nsISupp
 } // namespace
 
 namespace mozilla {
 namespace AvailableMemoryTracker {
 
 void
 Activate()
 {
-#if defined(_M_IX86) && defined(XP_WIN)
+#if defined(XP_WIN) && !defined(HAVE_64BIT_BUILD)
   MOZ_ASSERT(sInitialized);
   MOZ_ASSERT(!sHooksActive);
 
   RegisterStrongMemoryReporter(new LowEventsReporter());
   RegisterLowMemoryEventsVirtualDistinguishedAmount(
     LowMemoryEventsVirtualDistinguishedAmount);
   RegisterLowMemoryEventsPhysicalDistinguishedAmount(
     LowMemoryEventsPhysicalDistinguishedAmount);
   sHooksActive = true;
-#endif
+#endif // defined(XP_WIN) && !defined(HAVE_64BIT_BUILD)
 
-  // This object is held alive by the observer service.
+  // The watchers are held alive by the observer service.
   RefPtr<nsMemoryPressureWatcher> watcher = new nsMemoryPressureWatcher();
   watcher->Init();
+
+#if defined(XP_WIN) && defined(HAVE_64BIT_BUILD)
+  if (XRE_IsParentProcess()) {
+    RefPtr<nsAvailableMemoryWatcher> poller = new nsAvailableMemoryWatcher();
+
+    if (NS_FAILED(poller->Init())) {
+      NS_WARNING("Could not start the available memory watcher");
+    }
+  }
+#endif // defined(XP_WIN) && defined(HAVE_64BIT_BUILD)
 }
 
 void
 Init()
 {
   // Do nothing on x86-64, because nsWindowsDllInterceptor is not thread-safe
   // on 64-bit.  (On 32-bit, it's probably thread-safe.)  Even if we run Init()
   // before any other of our threads are running, another process may have
   // started a remote thread which could call VirtualAlloc!
   //
   // Moreover, the benefit of this code is less clear when we're a 64-bit
   // process, because we aren't going to run out of virtual memory, and the
   // system is likely to have a fair bit of physical memory.
 
-#if defined(_M_IX86) && defined(XP_WIN)
+#if defined(XP_WIN) && !defined(HAVE_64BIT_BUILD)
   // Don't register the hooks if we're a build instrumented for PGO: If we're
   // an instrumented build, the compiler adds function calls all over the place
   // which may call VirtualAlloc; this makes it hard to prevent
   // VirtualAllocHook from reentering itself.
   if (!PR_GetEnv("MOZ_PGO_INSTRUMENTED")) {
     sKernel32Intercept.Init("Kernel32.dll");
     sKernel32Intercept.AddHook("VirtualAlloc",
                                reinterpret_cast<intptr_t>(VirtualAllocHook),
@@ -441,13 +643,13 @@ Init()
 
     sGdi32Intercept.Init("Gdi32.dll");
     sGdi32Intercept.AddHook("CreateDIBSection",
                             reinterpret_cast<intptr_t>(CreateDIBSectionHook),
                             reinterpret_cast<void**>(&sCreateDIBSectionOrig));
   }
 
   sInitialized = true;
-#endif
+#endif // defined(XP_WIN) && !defined(HAVE_64BIT_BUILD)
 }
 
 } // namespace AvailableMemoryTracker
 } // namespace mozilla