Skip to content

Adaptive polling for low commit and memory check. - #249

Open
savemyram wants to merge 2 commits into
Eclipse-Community:win-153from
savemyram:win-153
Open

savemyram wants to merge 2 commits into
Eclipse-Community:win-153from
savemyram:win-153

Conversation

@savemyram

Copy link
Copy Markdown

No description provided.

@savemyram

savemyram commented Aug 13, 2026

Copy link
Copy Markdown
Author

Currently pondering the idea of adding another piece to this that would initiate an unload in between the ramp window if a drop of X of available memory is detected. It would kind of be like a soft unload to absorb sudden swings in RAM use in excess of a (configurable?) amount caused by visiting heavy sites or misbehaving background tabs.

Edit: Yeah...going to be testing a new approach that tries to to calculate the rate of RAM depletion rate and unload based on that and see how it goes. I'm trying to come up with something a bit more graceful, where it gracefully sheds the load as the threshold is closer instead of unloading when right on top of it.

@the-r3dacted Completely forgot you had disabled the tab unloading preference and was chasing my tail trying to figure out why my new test code wasn't unloading despite the log showing it was calling the unload command.

@savemyram

savemyram commented Aug 14, 2026

Copy link
Copy Markdown
Author

@the-r3dacted Currently testing predictive detection on Firefox 115 ESR source and it's...yielding interesting results:

/* 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 "mozilla/Atomics.h"
#include "mozilla/Mutex.h"
#include "mozilla/Preferences.h"
#include "mozilla/Services.h"
#include "mozilla/StaticPrefs_browser.h"
#include "mozilla/TimeStamp.h"
#include "nsAppRunner.h"
#include "nsExceptionHandler.h"
#include "nsIConsoleService.h"
#include "nsICrashReporter.h"
#include "nsIObserver.h"
#include "nsISupports.h"
#include "nsITimer.h"
#include "nsMemoryPressure.h"
#include "nsPrintfCString.h"
#include "nsServiceManagerUtils.h"
#include "nsString.h"
#include "nsThreadUtils.h"

#include <algorithm>
#include <cmath>
#include <memoryapi.h>
#include <windows.h>

extern mozilla::Atomic<uint32_t, mozilla::MemoryOrdering::Relaxed>
    sNumLowPhysicalMemEvents;

namespace mozilla {

namespace {

// Defaults used when the user has not created/changed the about:config prefs.
constexpr uint32_t kDefaultInitialPollingIntervalMs = 500;

// The shipped/default pref may override this value. The C++ fallback is
// intentionally zero; PhysicalMemoryPollRampWindowMB() clamps non-zero
// runtime values to the supported range.
constexpr uint32_t kDefaultPhysicalMemoryPollRampWindowMB = 0;

constexpr uint32_t kDefaultMinPollingIntervalMs = 100;
constexpr uint32_t kDefaultMaxPollingIntervalMs = 3000;
constexpr uint32_t kDefaultLowPhysicalMemoryThresholdMB = 512;

// Predictive unloading:
//
// If the current downward trend indicates that the low-memory threshold
// will be crossed within this amount of time, begin unloading immediately.
constexpr uint32_t kDefaultPredictionHorizonMs = 1000;

// Weight given to the newest instantaneous memory-loss rate.
//
// A value of 0.5 means:
//
//   smoothed = 0.5 * old + 0.5 * new
//
// This prevents one noisy GlobalMemoryStatusEx() sample from completely
// determining the prediction while still allowing the estimate to react
// quickly to a real memory-consumption spike.
constexpr double kPredictionRateSmoothingFactor = 0.5;

// Guardrails for user-created about:config values.
constexpr uint32_t kMinimumPhysicalMemoryPollRampWindowMB = 512;
constexpr uint32_t kMaximumPhysicalMemoryPollRampWindowMB = 64 * 1024;
constexpr uint32_t kMinimumPollingIntervalMs = 1000;
constexpr uint32_t kMaximumPollingIntervalMs = 10 * 60 * 1000;

const char kLowPhysicalMemoryThresholdMBPref[] =
    "browser.low_physical_memory_threshold_mb";

const char kPhysicalMemoryPollRampWindowMBPref[] =
    "browser.low_physical_memory_poll_ramp_window_mb";

const char kInitialPollingIntervalMsPref[] =
    "browser.low_memory_polling_initial_interval_ms";

const char kMinPollingIntervalMsPref[] =
    "browser.low_memory_polling_min_interval_ms";

const char kMaxPollingIntervalMsPref[] =
    "browser.low_memory_polling_max_interval_ms";

// Test-only instrumentation. Set to false before landing.
constexpr bool kLogPollingToBrowserConsole = true;

uint32_t ClampUint32(uint32_t aValue, uint32_t aMin, uint32_t aMax) {
  if (aValue < aMin) {
    return aMin;
  }

  if (aValue > aMax) {
    return aMax;
  }

  return aValue;
}

void LogPollingToBrowserConsole(const nsACString& aMessage) {
  if (!kLogPollingToBrowserConsole) {
    return;
  }

  nsCOMPtr<nsIConsoleService> console =
      do_GetService(NS_CONSOLESERVICE_CONTRACTID);

  if (!console) {
    return;
  }

  nsCString utf8Message("AvailableMemoryWatcher: ");
  utf8Message.Append(aMessage);

  NS_ConvertUTF8toUTF16 message(utf8Message);
  console->LogStringMessage(message.get());
}

uint32_t LowPhysicalMemoryThresholdMB() {
  return Preferences::GetUint(kLowPhysicalMemoryThresholdMBPref,
                              kDefaultLowPhysicalMemoryThresholdMB);
}

uint32_t PhysicalMemoryPollRampWindowMB() {
  return ClampUint32(
      Preferences::GetUint(kPhysicalMemoryPollRampWindowMBPref,
                           kDefaultPhysicalMemoryPollRampWindowMB),
      kMinimumPhysicalMemoryPollRampWindowMB,
      kMaximumPhysicalMemoryPollRampWindowMB);
}

uint32_t MinPollingIntervalMs() {
  return ClampUint32(
      Preferences::GetUint(kMinPollingIntervalMsPref,
                           kDefaultMinPollingIntervalMs),
      kMinimumPollingIntervalMs,
      kMaximumPollingIntervalMs);
}

uint32_t MaxPollingIntervalMs() {
  uint32_t minPollingIntervalMs = MinPollingIntervalMs();

  return ClampUint32(
      Preferences::GetUint(kMaxPollingIntervalMsPref,
                           kDefaultMaxPollingIntervalMs),
      minPollingIntervalMs,
      kMaximumPollingIntervalMs);
}

uint32_t InitialPollingIntervalMs() {
  if (gIsGtest) {
    return 10;
  }

  return ClampUint32(
      Preferences::GetUint(kInitialPollingIntervalMsPref,
                           kDefaultInitialPollingIntervalMs),
      kMinimumPollingIntervalMs,
      kMaximumPollingIntervalMs);
}

void LogCurrentPrefValues() {
  LogPollingToBrowserConsole(nsPrintfCString(
      "pref values: initial=%u ms, min=%u ms, max=%u ms, ramp=%u MB, "
      "threshold=%u MB, predictionHorizon=%u ms",
      InitialPollingIntervalMs(),
      MinPollingIntervalMs(),
      MaxPollingIntervalMs(),
      PhysicalMemoryPollRampWindowMB(),
      LowPhysicalMemoryThresholdMB(),
      kDefaultPredictionHorizonMs));
}

}  // namespace

class nsAvailableMemoryWatcher final : public nsITimerCallback,
                                       public nsINamed,
                                       public nsAvailableMemoryWatcherBase {
 public:
  NS_DECL_ISUPPORTS_INHERITED
  NS_DECL_NSIOBSERVER
  NS_DECL_NSITIMERCALLBACK
  NS_DECL_NSINAMED

  nsAvailableMemoryWatcher();
  nsresult Init() override;

  // Called by TabUnloader when an asynchronous unload attempt finishes.
  nsresult OnUnloadAttemptCompleted(nsresult aResult) override;

 private:
  static void RecordLowMemoryEvent();

  static bool IsCommitSpaceLow();
  static bool IsPhysicalMemoryLow();
  static bool IsMemoryLow();

  uint32_t GetAdaptivePollingIntervalMs(
      bool aAlreadyUnderMemoryPressure);

  bool ShouldUnloadPredictively(const MutexAutoLock& aLock);

  void OnPredictiveUnload(const MutexAutoLock& aLock);

  void UpdatePhysicalMemorySample(uint64_t aAvailPhysBytes,
                                  const TimeStamp& aNow);

  void ResetPhysicalMemoryPrediction();

  ~nsAvailableMemoryWatcher();

  Mutex mMutex;

  void MaybeSaveMemoryReport(const MutexAutoLock&) MOZ_REQUIRES(mMutex);
  void Shutdown(const MutexAutoLock&) MOZ_REQUIRES(mMutex);

  void ScheduleNextPoll(const MutexAutoLock&) MOZ_REQUIRES(mMutex);
  void LogPollFired(const MutexAutoLock&) MOZ_REQUIRES(mMutex);

  void OnLowMemory(const MutexAutoLock&) MOZ_REQUIRES(mMutex);
  void OnHighMemory(const MutexAutoLock&) MOZ_REQUIRES(mMutex);

  nsCOMPtr<nsITimer> mTimer MOZ_GUARDED_BY(mMutex);

  bool mUnderMemoryPressure MOZ_GUARDED_BY(mMutex);
  bool mSavedReport MOZ_GUARDED_BY(mMutex);
  bool mIsShutdown MOZ_GUARDED_BY(mMutex);

  // True while a predictive unload is still in progress.
  //
  // This is cleared by OnUnloadAttemptCompleted(), which is called by
  // TabUnloader when the actual asynchronous unload attempt finishes.
  bool mPredictiveUnloadInProgress MOZ_GUARDED_BY(mMutex);

  TimeStamp mLastPollTime MOZ_GUARDED_BY(mMutex);

  // Previous available physical-memory sample used to calculate the
  // instantaneous rate at which available physical memory is falling.
  uint64_t mPreviousAvailPhysBytes MOZ_GUARDED_BY(mMutex);

  TimeStamp mPreviousAvailPhysTime MOZ_GUARDED_BY(mMutex);

  // Smoothed downward rate of available physical memory, in bytes/sec.
  //
  // A positive value means available physical memory is falling.
  // Zero means there is currently no usable downward trend.
  double mSmoothedDropRateBytesPerSecond MOZ_GUARDED_BY(mMutex);
};

NS_IMPL_ISUPPORTS_INHERITED(nsAvailableMemoryWatcher,
                            nsAvailableMemoryWatcherBase,
                            nsIObserver,
                            nsITimerCallback,
                            nsINamed)

nsAvailableMemoryWatcher::nsAvailableMemoryWatcher()
    : mMutex("nsAvailableMemoryWatcher::mMutex"),
      mUnderMemoryPressure(false),
      mSavedReport(false),
      mIsShutdown(false),
      mPredictiveUnloadInProgress(false),
      mPreviousAvailPhysBytes(0),
      mSmoothedDropRateBytesPerSecond(0.0) {}

nsresult nsAvailableMemoryWatcher::Init() {
  nsresult rv = nsAvailableMemoryWatcherBase::Init();
  if (NS_FAILED(rv)) {
    return rv;
  }

  MutexAutoLock lock(mMutex);

  mTimer = NS_NewTimer();
  if (!mTimer) {
    return NS_ERROR_OUT_OF_MEMORY;
  }

  uint32_t initialIntervalMs = InitialPollingIntervalMs();

  LogCurrentPrefValues();

  LogPollingToBrowserConsole(nsPrintfCString(
      "initial scheduling in %u ms", initialIntervalMs));

  rv = mTimer->InitWithCallback(this, initialIntervalMs,
                                nsITimer::TYPE_ONE_SHOT);

  if (NS_FAILED(rv)) {
    return rv;
  }

  return NS_OK;
}

nsAvailableMemoryWatcher::~nsAvailableMemoryWatcher() = default;

void nsAvailableMemoryWatcher::RecordLowMemoryEvent() {
  sNumLowPhysicalMemEvents++;
}

void nsAvailableMemoryWatcher::Shutdown(const MutexAutoLock&) {
  mIsShutdown = true;
  mPredictiveUnloadInProgress = false;
  ResetPhysicalMemoryPrediction();

  if (mTimer) {
    mTimer->Cancel();
    mTimer = nullptr;
  }
}

void nsAvailableMemoryWatcher::MaybeSaveMemoryReport(
    const MutexAutoLock&) {
  if (mSavedReport) {
    return;
  }

  if (nsCOMPtr<nsICrashReporter> cr =
          do_GetService("@mozilla.org/toolkit/crash-reporter;1")) {
    mSavedReport = NS_SUCCEEDED(cr->SaveMemoryReport());
  }
}

void nsAvailableMemoryWatcher::LogPollFired(
    const MutexAutoLock&) {
  TimeStamp now = TimeStamp::Now();

  if (!mLastPollTime.IsNull()) {
    double elapsedMs =
        (now - mLastPollTime).ToMilliseconds();

    LogPollingToBrowserConsole(nsPrintfCString(
        "poll fired after %.1f ms; underMemoryPressure=%d, "
        "predictiveUnloadInProgress=%d",
        elapsedMs,
        static_cast<int>(mUnderMemoryPressure),
        static_cast<int>(mPredictiveUnloadInProgress)));
  } else {
    LogPollingToBrowserConsole(
        nsCString("initial poll fired"));
  }

  mLastPollTime = now;
}

void nsAvailableMemoryWatcher::ScheduleNextPoll(
    const MutexAutoLock&) {
  if (mIsShutdown || !mTimer) {
    return;
  }

  uint32_t interval =
      GetAdaptivePollingIntervalMs(mUnderMemoryPressure);

  LogPollingToBrowserConsole(nsPrintfCString(
      "scheduling next poll in %u ms; underMemoryPressure=%d",
      interval,
      static_cast<int>(mUnderMemoryPressure)));

  mTimer->InitWithCallback(this, interval,
                           nsITimer::TYPE_ONE_SHOT);
}

void nsAvailableMemoryWatcher::OnLowMemory(
    const MutexAutoLock& aLock) {
  if (!mUnderMemoryPressure) {
    RecordLowMemoryEvent();
  }

  mUnderMemoryPressure = true;

  if (NS_IsMainThread()) {
    MaybeSaveMemoryReport(aLock);
    UpdateLowMemoryTimeStamp();

    {
      // Do not hold mMutex while calling the tab unloader. The tab unloader
      // can synchronously call back into the memory watcher.
      MutexAutoUnlock unlock(mMutex);
      mTabUnloader->UnloadTabAsync();
    }
  } else {
    NS_DispatchToMainThread(NS_NewRunnableFunction(
        "nsAvailableMemoryWatcher::OnLowMemory",
        [self = RefPtr<nsAvailableMemoryWatcher>(this)]() {
          {
            MutexAutoLock lock(self->mMutex);
            self->MaybeSaveMemoryReport(lock);
            self->UpdateLowMemoryTimeStamp();
          }

          self->mTabUnloader->UnloadTabAsync();
        }));
  }
}

void nsAvailableMemoryWatcher::OnHighMemory(
    const MutexAutoLock&) {
  MOZ_ASSERT(NS_IsMainThread());

  if (mUnderMemoryPressure) {
    RecordTelemetryEventOnHighMemory();

    NS_NotifyOfEventualMemoryPressure(
        MemoryPressureState::NoPressure);
  }

  mUnderMemoryPressure = false;
  mSavedReport = false;
}

void nsAvailableMemoryWatcher::ResetPhysicalMemoryPrediction() {
  mPreviousAvailPhysBytes = 0;
  mPreviousAvailPhysTime = TimeStamp();
  mSmoothedDropRateBytesPerSecond = 0.0;
}

void nsAvailableMemoryWatcher::UpdatePhysicalMemorySample(
    uint64_t aAvailPhysBytes,
    const TimeStamp& aNow) {
  /*
   * We only have a meaningful rate if we have a previous sample.
   */
  if (!mPreviousAvailPhysTime.IsNull()) {
    double elapsedSeconds =
        (aNow - mPreviousAvailPhysTime).ToSeconds();

    if (elapsedSeconds > 0.0 &&
        std::isfinite(elapsedSeconds)) {
      /*
       * If available memory increased, there is no current downward
       * trend. Reset the prediction rather than allowing an old negative
       * trend to remain active after memory has recovered.
       */
      if (aAvailPhysBytes >= mPreviousAvailPhysBytes) {
        mSmoothedDropRateBytesPerSecond = 0.0;
      } else {
        const double instantaneousDropRate =
            static_cast<double>(
                mPreviousAvailPhysBytes - aAvailPhysBytes) /
            elapsedSeconds;

        if (std::isfinite(instantaneousDropRate) &&
            instantaneousDropRate > 0.0) {
          if (mSmoothedDropRateBytesPerSecond > 0.0) {
            mSmoothedDropRateBytesPerSecond =
                (kPredictionRateSmoothingFactor *
                 instantaneousDropRate) +
                ((1.0 - kPredictionRateSmoothingFactor) *
                 mSmoothedDropRateBytesPerSecond);
          } else {
            mSmoothedDropRateBytesPerSecond =
                instantaneousDropRate;
          }
        } else {
          mSmoothedDropRateBytesPerSecond = 0.0;
        }
      }
    }
  }

  mPreviousAvailPhysBytes = aAvailPhysBytes;
  mPreviousAvailPhysTime = aNow;
}

bool nsAvailableMemoryWatcher::ShouldUnloadPredictively(
    const MutexAutoLock& aLock) {
  MEMORYSTATUSEX memStatus = {sizeof(memStatus)};

  if (!::GlobalMemoryStatusEx(&memStatus)) {
    LogPollingToBrowserConsole(
        nsCString(
            "predictive check: GlobalMemoryStatusEx failed"));
    return false;
  }

  constexpr uint64_t MB = 1024 * 1024;

  const uint64_t availPhysBytes =
      memStatus.ullAvailPhys;

  const TimeStamp now = TimeStamp::Now();

  const uint32_t thresholdMB =
      LowPhysicalMemoryThresholdMB();

  if (thresholdMB == 0) {
    ResetPhysicalMemoryPrediction();
    return false;
  }

  const uint64_t thresholdBytes =
      static_cast<uint64_t>(thresholdMB) * MB;

  /*
   * We only use prediction while above the hard threshold.
   *
   * If we're already below it, IsPhysicalMemoryLow() / OnLowMemory()
   * is responsible for handling the condition.
   */
  if (availPhysBytes <= thresholdBytes) {
    UpdatePhysicalMemorySample(availPhysBytes, now);
    return false;
  }

  /*
   * Update the rate estimate using the current sample.
   *
   * The predictor therefore always operates on the most recent available
   * physical-memory trend.
   */
  const uint64_t previousAvailPhysBytes =
      mPreviousAvailPhysBytes;

  const TimeStamp previousAvailPhysTime =
      mPreviousAvailPhysTime;

  UpdatePhysicalMemorySample(availPhysBytes, now);

  if (previousAvailPhysTime.IsNull()) {
    LogPollingToBrowserConsole(
        nsCString(
            "predictive check: waiting for second sample"));
    return false;
  }

  const double dropRateBytesPerSecond =
      mSmoothedDropRateBytesPerSecond;

  if (dropRateBytesPerSecond <= 0.0 ||
      !std::isfinite(dropRateBytesPerSecond)) {
    LogPollingToBrowserConsole(
        nsCString(
            "predictive check: no downward memory trend"));
    return false;
  }

  const uint64_t bytesAboveThreshold =
      availPhysBytes - thresholdBytes;

  const double secondsUntilThreshold =
      static_cast<double>(bytesAboveThreshold) /
      dropRateBytesPerSecond;

  const double predictionHorizonSeconds =
      static_cast<double>(kDefaultPredictionHorizonMs) /
      1000.0;

  const double availableMB =
      static_cast<double>(availPhysBytes) /
      static_cast<double>(MB);

  const double dropRateMBPerSecond =
      dropRateBytesPerSecond /
      static_cast<double>(MB);

  LogPollingToBrowserConsole(nsPrintfCString(
      "predictive check: availPhys=%.1f MB, "
      "threshold=%u MB, dropRate=%.1f MB/s, "
      "secondsUntilThreshold=%.3f, horizon=%.3f",
      availableMB,
      thresholdMB,
      dropRateMBPerSecond,
      secondsUntilThreshold,
      predictionHorizonSeconds));

  /*
   * Straight predictive policy:
   *
   * If the current smoothed downward trend would cross the hard
   * threshold within the prediction horizon, unload now.
   */
  return secondsUntilThreshold <= predictionHorizonSeconds;
}

void nsAvailableMemoryWatcher::OnPredictiveUnload(
    const MutexAutoLock& aLock) {
  if (NS_IsMainThread()) {
    /*
     * Do not hold mMutex while invoking the unloader. The unloader can
     * call OnUnloadAttemptCompleted(), which needs mMutex.
     */
    MutexAutoUnlock unlock(mMutex);
    mTabUnloader->UnloadTabAsync();
    return;
  }

  /*
   * We're not on the main thread. The flag has already been set while
   * holding mMutex, so another poll cannot queue another unload.
   */
  NS_DispatchToMainThread(NS_NewRunnableFunction(
      "nsAvailableMemoryWatcher::OnPredictiveUnload",
      [self = RefPtr<nsAvailableMemoryWatcher>(this)]() {
        self->mTabUnloader->UnloadTabAsync();
      }));
}

NS_IMETHODIMP
nsAvailableMemoryWatcher::OnUnloadAttemptCompleted(
    nsresult aResult) {
  MutexAutoLock lock(mMutex);

  /*
   * The actual asynchronous unload has finished.
   *
   * This is the authoritative signal that allows another predictive
   * unload to be requested.
   */
  mPredictiveUnloadInProgress = false;

  switch (aResult) {
    case NS_OK:
      LogPollingToBrowserConsole(
          nsCString(
              "predictive unload completed successfully"));
      break;

    case NS_ERROR_NOT_AVAILABLE:
      LogPollingToBrowserConsole(
          nsCString(
              "predictive unload completed: no unloadable tab"));
      break;

    case NS_ERROR_ABORT:
      LogPollingToBrowserConsole(
          nsCString(
              "predictive unload skipped: unload already in progress"));
      break;

    default:
      LogPollingToBrowserConsole(
          nsPrintfCString(
              "predictive unload completed with result=0x%08x",
              static_cast<uint32_t>(aResult)));
      break;
  }

  return NS_OK;
}

bool nsAvailableMemoryWatcher::IsCommitSpaceLow() {
  MEMORYSTATUSEX memStatus = {sizeof(memStatus)};

  if (!::GlobalMemoryStatusEx(&memStatus)) {
    return false;
  }

  constexpr uint64_t MB = 1024 * 1024;

  uint64_t availCommitMB =
      memStatus.ullAvailPageFile / MB;

  return availCommitMB <
         StaticPrefs::browser_low_commit_space_threshold_mb();
}

bool nsAvailableMemoryWatcher::IsPhysicalMemoryLow() {
  uint32_t thresholdMB =
      LowPhysicalMemoryThresholdMB();

  if (thresholdMB == 0) {
    return false;
  }

  MEMORYSTATUSEX memStatus = {sizeof(memStatus)};

  if (!::GlobalMemoryStatusEx(&memStatus)) {
    return false;
  }

  constexpr uint64_t MB = 1024 * 1024;

  uint64_t availPhysMB =
      memStatus.ullAvailPhys / MB;

  return availPhysMB < thresholdMB;
}

bool nsAvailableMemoryWatcher::IsMemoryLow() {
  return IsCommitSpaceLow() ||
         IsPhysicalMemoryLow();
}

uint32_t nsAvailableMemoryWatcher::GetAdaptivePollingIntervalMs(
    bool aAlreadyUnderMemoryPressure) {
  if (gIsGtest) {
    return 10;
  }

  LogCurrentPrefValues();

  uint32_t lowMemoryPollingInterval =
      MinPollingIntervalMs();

  uint32_t healthyPollingInterval =
      MaxPollingIntervalMs();

  if (aAlreadyUnderMemoryPressure) {
    LogPollingToBrowserConsole(nsPrintfCString(
        "adaptive poll calculation: already under memory pressure, "
        "interval=%u ms",
        lowMemoryPollingInterval));

    return lowMemoryPollingInterval;
  }

  MEMORYSTATUSEX memStatus = {sizeof(memStatus)};

  if (!::GlobalMemoryStatusEx(&memStatus)) {
    LogPollingToBrowserConsole(nsPrintfCString(
        "adaptive poll calculation: GlobalMemoryStatusEx failed, "
        "interval=%u ms",
        healthyPollingInterval));

    return healthyPollingInterval;
  }

  constexpr uint64_t MB = 1024 * 1024;

  uint64_t availPhysMB =
      memStatus.ullAvailPhys / MB;

  uint64_t fastPollingTargetMB =
      LowPhysicalMemoryThresholdMB();

  if (fastPollingTargetMB == 0) {
    LogPollingToBrowserConsole(nsPrintfCString(
        "adaptive poll calculation: physical-memory threshold "
        "disabled, interval=%u ms",
        healthyPollingInterval));

    return healthyPollingInterval;
  }

  uint64_t rampWindowMB =
      PhysicalMemoryPollRampWindowMB();

  uint64_t rampStartMB =
      fastPollingTargetMB + rampWindowMB;

  /*
   * Above the configured warning window, use normal polling.
   *
   * The ramp is now used only for polling frequency. It does not itself
   * trigger an unload.
   */
  if (availPhysMB >= rampStartMB) {
    LogPollingToBrowserConsole(nsPrintfCString(
        "adaptive poll calculation: availPhys=%llu MB, "
        "rampStart=%llu MB, fastTarget=%llu MB, interval=%u ms",
        static_cast<unsigned long long>(
            availPhysMB),
        static_cast<unsigned long long>(
            rampStartMB),
        static_cast<unsigned long long>(
            fastPollingTargetMB),
        healthyPollingInterval));

    return healthyPollingInterval;
  }

  /*
   * At or below the hard threshold, use the fastest polling interval.
   */
  if (availPhysMB <= fastPollingTargetMB) {
    LogPollingToBrowserConsole(nsPrintfCString(
        "adaptive poll calculation: availPhys=%llu MB, "
        "rampStart=%llu MB, fastTarget=%llu MB, interval=%u ms",
        static_cast<unsigned long long>(
            availPhysMB),
        static_cast<unsigned long long>(
            rampStartMB),
        static_cast<unsigned long long>(
            fastPollingTargetMB),
        lowMemoryPollingInterval));

    return lowMemoryPollingInterval;
  }

  /*
   * Quadratic polling ramp:
   *
   *   At rampStart -> slow polling.
   *   At threshold -> fast polling.
   *
   * This has no direct bearing on whether a predictive unload occurs.
   * It only controls how frequently we get another sample.
   */
  uint64_t gapMB =
      availPhysMB - fastPollingTargetMB;

  uint64_t intervalRange =
      healthyPollingInterval -
      lowMemoryPollingInterval;

  const uint64_t rampWindowSquared =
      rampWindowMB * rampWindowMB;

  uint64_t interval =
      lowMemoryPollingInterval +
      intervalRange * gapMB * gapMB /
          rampWindowSquared;

  uint32_t adaptiveInterval =
      static_cast<uint32_t>(interval);

  LogPollingToBrowserConsole(nsPrintfCString(
      "adaptive poll calculation: availPhys=%llu MB, "
      "rampStart=%llu MB, fastTarget=%llu MB, interval=%u ms",
      static_cast<unsigned long long>(
          availPhysMB),
      static_cast<unsigned long long>(
          rampStartMB),
      static_cast<unsigned long long>(
          fastPollingTargetMB),
      adaptiveInterval));

  return adaptiveInterval;
}

NS_IMETHODIMP
nsAvailableMemoryWatcher::Notify(nsITimer* aTimer) {
  MutexAutoLock lock(mMutex);

  if (mIsShutdown) {
    return NS_OK;
  }

  LogPollFired(lock);

  if (IsMemoryLow()) {
    /*
     * Actual low-memory condition.
     *
     * This is the existing hard-pressure path.
     */
    OnLowMemory(lock);
  } else {
    /*
     * We're above the hard threshold. Do not mark the browser as being
     * under memory pressure merely because we're approaching it.
     */
    OnHighMemory(lock);

    /*
     * The predictive path is independent of the polling ramp.
     *
     * The ramp only makes us sample more frequently as we approach the
     * threshold. The predictor decides whether the memory trend is
     * dangerous enough to unload.
     */
    if (!mPredictiveUnloadInProgress &&
        ShouldUnloadPredictively(lock)) {
      mPredictiveUnloadInProgress = true;

      LogPollingToBrowserConsole(
          nsCString(
              "PREDICTIVE unload triggered"));

      OnPredictiveUnload(lock);
    }
  }

  ScheduleNextPoll(lock);

  return NS_OK;
}

NS_IMETHODIMP
nsAvailableMemoryWatcher::GetName(nsACString& aName) {
  aName.AssignLiteral("nsAvailableMemoryWatcher");
  return NS_OK;
}

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);
  }

  return NS_OK;
}

already_AddRefed<nsAvailableMemoryWatcherBase>
CreateAvailableMemoryWatcher() {
  RefPtr<nsAvailableMemoryWatcher> watcher =
      new nsAvailableMemoryWatcher();

  if (NS_FAILED(watcher->Init())) {
    return do_AddRef(
        new nsAvailableMemoryWatcherBase);
  }

  return watcher.forget();
}

}  // namespace mozillala

Edit: Replaced with new code currently being tested.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant