Commit df91a347 authored by kriswright's avatar kriswright
Browse files

Bug 1532955 - Track available memory on linux. r=gsvelto,tkikuchi

This introduces a low memory watcher that dispatches an offthread read of /proc/meminfo every 5000/1000ms depending on memory levels, then determines which information to act on. It works like this:
- Get a percentage of `MemAvailable` versus `MemTotal`.
- If memory drops below 5% availability, we are in a memory pressure scenario
- If `MemAvailable` is not large enough to accommodate a content process, we are in a memory pressure scenario
- If we are in a memory pressure scenario, notify the observers from the main thread.

The value I decided to use to represent a content process was based on observation and should be adjusted if it is not representative of what we consider a "typical" content process.

Differential Revision: https://phabricator.services.mozilla.com/D117972
parent 56a43f9e
Loading
Loading
Loading
Loading
+1 −0
Original line number Diff line number Diff line
@@ -46,6 +46,7 @@ MWQThread
MediaCache
MediaTelemetry
MediaTrackGrph
MemoryPoller
mtransport
NamedPipeSrv
Netlink Monitor
+11 −1
Original line number Diff line number Diff line
@@ -1180,7 +1180,7 @@
  value: 16777216
  mirror: always

#ifdef XP_WIN
#if defined(XP_WIN) || defined(XP_LINUX)
  # Notify TabUnloader or send the memory pressure if the memory resource
  # notification is signaled AND the available commit space is lower than
  # this value.
@@ -1190,6 +1190,16 @@
    mirror: always
#endif

#ifdef XP_LINUX
  # On Linux we also check available memory in comparison to total memory,
  # and use this percent value (out of 100) to determine if we are in a
  # low memory scenario.
-   name: browser.low_commit_space_threshold_percent
    type: RelaxedAtomicUint32
    value: 5
    mirror: always
#endif

# Render animations and videos as a solid color
- name: browser.measurement.render_anims_and_video_solid
  type: RelaxedAtomicBool
+2 −0
Original line number Diff line number Diff line
@@ -44,6 +44,8 @@ skip-if = os != 'win'
[TestMacroForEach]
[TestMathAlgorithms]
[TestMaybe]
[TestMemoryPressureWatcherLinux]
skip-if = os != 'linux'
[TestMMPolicy]
skip-if = os != 'win'
[TestNativeNt]
+2 −1
Original line number Diff line number Diff line
@@ -169,7 +169,8 @@ void nsAvailableMemoryWatcherBase::RecordTelemetryEventOnHighMemory() {

// Define the fallback method for a platform for which a platform-specific
// CreateAvailableMemoryWatcher() is not defined.
#if !defined(XP_WIN) && !defined(XP_MACOSX)
#if defined(ANDROID) || \
    !defined(XP_WIN) && !defined(XP_MACOSX) && !defined(XP_LINUX)
already_AddRefed<nsAvailableMemoryWatcherBase> CreateAvailableMemoryWatcher() {
  RefPtr instance(new nsAvailableMemoryWatcherBase);
  return do_AddRef(instance);
+255 −0
Original line number Diff line number Diff line
/* -*- 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(const MutexAutoLock&);
  static bool IsMemoryLow();

  nsCOMPtr<nsITimer> mTimer;
  nsCOMPtr<nsIThread> mThread;

  bool mPolling;
  bool mUnderMemoryPressure;

  // We might tell polling to start/stop from our polling thread
  // or from the main thread during ::Observe().
  Mutex 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),
      mMutex("Memory Poller mutex") {}

nsresult nsAvailableMemoryWatcher::Init() {
  nsresult rv = nsAvailableMemoryWatcherBase::Init();
  if (NS_FAILED(rv)) {
    return rv;
  }
  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;

  MutexAutoLock lock(mMutex);
  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);

void nsAvailableMemoryWatcher::StopPolling(const MutexAutoLock&) {
  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};
  bool aResult = false;

  nsresult rv = ReadMemoryFile(kMeminfoPath, memInfo);

  if (NS_FAILED(rv) || memInfo.memAvailable == 0) {
    // If memAvailable cannot be found, then we are using an older system.
    // We can't accurately poll on this.
    return aResult;
  }
  unsigned long memoryAsPercentage =
      (memInfo.memAvailable * 100) / memInfo.memTotal;

  if (memoryAsPercentage <=
          StaticPrefs::browser_low_commit_space_threshold_percent() ||
      memInfo.memAvailable <
          StaticPrefs::browser_low_commit_space_threshold_mb() * 1024) {
    aResult = true;
  }

  return aResult;
}

void nsAvailableMemoryWatcher::ShutDown(const MutexAutoLock&) {
  if (mTimer) {
    mTimer->Cancel();
  }

  if (mThread) {
    mThread->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 (!mUnderMemoryPressure) {
    mUnderMemoryPressure = true;
    // 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.
  NS_DispatchToMainThread(NS_NewRunnableFunction(
      "nsAvailableMemoryWatcher::OnLowMemory",
      [self = RefPtr{this}]() { self->mTabUnloader->UnloadTabAsync(); }));
}

// 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 (mUnderMemoryPressure) {
    RecordTelemetryEventOnHighMemory();
    NS_NotifyOfEventualMemoryPressure(MemoryPressureState::NoPressure);
    mUnderMemoryPressure = false;
  }
  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) {
  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;
  }

  MutexAutoLock lock(mMutex);
  if (strcmp(aTopic, "xpcom-shutdown") == 0) {
    ShutDown(lock);
  } else 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
Loading