[Memory Pressure] Add critical pressure signal for low disk space on Mac

When a system is critically low on disk space, browser performance can
degrade significantly as the OS may be unable to swap memory to disk
effectively.

This CL introduces a mechanism in SystemMemoryPressureEvaluator on macOS
to monitor for low disk space. It periodically checks the free space on
the volume containing the user's home directory, which serves as a
reliable proxy for the volume storing browser data.

If the available space falls below a configurable threshold (default
250MB), a MEMORY_PRESSURE_LEVEL_CRITICAL signal is dispatched. This
allows the browser to proactively reduce its memory footprint before
disk-related performance issues occur.

The feature is controlled by the MacCriticalDiskSpacePressure feature
flag, which is disabled by default.

Bug: 393346737

Change-Id: I2111c224fcd0734a0ad9084c2855fc945e4e8721
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/6639007
Commit-Queue: Alex Attar <aattar@google.com>
Reviewed-by: Francois Pierre Doray <fdoray@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1476185}
diff --git a/components/memory_pressure/multi_source_memory_pressure_monitor_unittest.cc b/components/memory_pressure/multi_source_memory_pressure_monitor_unittest.cc
index 62b57655..cea7ade 100644
--- a/components/memory_pressure/multi_source_memory_pressure_monitor_unittest.cc
+++ b/components/memory_pressure/multi_source_memory_pressure_monitor_unittest.cc
@@ -20,8 +20,15 @@
 }
 
 TEST(MultiSourceMemoryPressureMonitorTest, RunDispatchCallback) {
+#if BUILDFLAG(IS_FUCHSIA)
+  // On Fuchsia, the previous SingleThreadTaskEnvironment was sufficient.
   base::test::SingleThreadTaskEnvironment task_environment(
       base::test::TaskEnvironment::MainThreadType::IO);
+#else
+  // On other platforms (like Mac), the full TaskEnvironment is needed for the
+  // ThreadPool.
+  base::test::TaskEnvironment task_environment;
+#endif
 
   MultiSourceMemoryPressureMonitor monitor;
   bool callback_called = false;
diff --git a/components/memory_pressure/system_memory_pressure_evaluator_mac.cc b/components/memory_pressure/system_memory_pressure_evaluator_mac.cc
index 45b4225..1da4316 100644
--- a/components/memory_pressure/system_memory_pressure_evaluator_mac.cc
+++ b/components/memory_pressure/system_memory_pressure_evaluator_mac.cc
@@ -9,6 +9,7 @@
 #include <stddef.h>
 #include <sys/sysctl.h>
 
+#include <algorithm>
 #include <cmath>
 
 #include "base/check_op.h"
@@ -16,7 +17,10 @@
 #include "base/functional/bind.h"
 #include "base/mac/mac_util.h"
 #include "base/memory/memory_pressure_monitor.h"
+#include "base/path_service.h"
 #include "base/task/sequenced_task_runner.h"
+#include "base/task/thread_pool.h"
+#include "base/time/time.h"
 
 namespace memory_pressure::mac {
 
@@ -27,6 +31,28 @@
 BASE_FEATURE(kSkipModerateMemoryPressureLevelMac,
              "SkipModerateMemoryPressureLevelMac",
              base::FEATURE_DISABLED_BY_DEFAULT);
+
+// This feature controls the critical memory pressure signal based on low disk
+// space. Disabling this feature turns off the disk space check entirely.
+BASE_FEATURE(kMacCriticalDiskSpacePressure,
+             "MacCriticalDiskSpacePressure",
+             base::FEATURE_DISABLED_BY_DEFAULT);
+
+// The default threshold in megabytes for the critical disk space pressure
+// signal.
+constexpr int kDefaultCriticalDiskSpaceMb = 250;
+const int64_t kBytesPerMb = 1024 * 1024;
+
+// Defines the threshold in megabytes for the critical disk space pressure
+// signal. This is a parameter for the kMacCriticalDiskSpacePressure feature.
+BASE_FEATURE_PARAM(int,
+                   kMacCriticalDiskSpacePressureThresholdMB,
+                   &kMacCriticalDiskSpacePressure,
+                   "MacCriticalDiskSpacePressureThresholdMB",
+                   kDefaultCriticalDiskSpaceMb);
+
+// How often to check for free disk space.
+constexpr base::TimeDelta kDiskSpaceCheckPeriod = base::Seconds(5);
 }  // namespace
 
 base::MemoryPressureListener::MemoryPressureLevel
@@ -61,7 +87,18 @@
           base::BindRepeating(&SystemMemoryPressureEvaluator::SendCurrentVote,
                               base::Unretained(this),
                               /*notify=*/true)),
+      disk_check_task_runner_(
+          base::ThreadPool::CreateSequencedTaskRunner({base::MayBlock()})),
       weak_ptr_factory_(this) {
+  // A check for available disk space is necessary to generate a
+  // low-disk-space pressure signal.
+  //
+  // To ensure the correct disk volume is checked, this implementation uses
+  // the user's home directory path, retrieved via `base::PathService`. On
+  // macOS, the browser's data directory is a subdirectory of home, so this
+  // correctly targets the volume most relevant to browser performance.
+  base::PathService::Get(base::DIR_HOME, &user_data_dir_);
+
   // WeakPtr needed because there is no guarantee that |this| is still be alive
   // when the task posted to the TaskRunner or event handler runs.
   base::WeakPtr<SystemMemoryPressureEvaluator> weak_this =
@@ -82,6 +119,15 @@
     // Start monitoring the event source.
     dispatch_resume(memory_level_event_source_.get());
   }
+
+  if (base::FeatureList::IsEnabled(kMacCriticalDiskSpacePressure)) {
+    disk_space_check_timer_.Start(
+        FROM_HERE, kDiskSpaceCheckPeriod,
+        base::BindRepeating(&SystemMemoryPressureEvaluator::CheckDiskSpace,
+                            weak_this));
+    // Perform an initial check on startup.
+    CheckDiskSpace();
+  }
 }
 
 SystemMemoryPressureEvaluator::~SystemMemoryPressureEvaluator() {
@@ -106,12 +152,55 @@
   DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
   // Get the current macOS pressure level and convert to the corresponding
   // Chrome pressure level.
-  SetCurrentVote(MemoryPressureLevelForMacMemoryPressureLevel(
-      GetMacMemoryPressureLevel()));
+  auto os_pressure_level =
+      MemoryPressureLevelForMacMemoryPressureLevel(GetMacMemoryPressureLevel());
+
+  // The effective pressure level is the most severe of the OS-reported level
+  // and our disk-space-derived level. If the disk pressure feature is disabled,
+  // `disk_pressure_vote_` will always be `NONE`.
+  auto effective_pressure_level =
+      std::max(os_pressure_level, disk_pressure_vote_);
+
+  SetCurrentVote(effective_pressure_level);
 }
 
 void SystemMemoryPressureEvaluator::OnMemoryPressureChanged() {
   DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
+  UpdatePressureAndManageNotifications();
+}
+
+void SystemMemoryPressureEvaluator::CheckDiskSpace() {
+  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
+  disk_check_task_runner_->PostTaskAndReplyWithResult(
+      FROM_HERE,
+      base::BindOnce(&base::SysInfo::AmountOfFreeDiskSpace, user_data_dir_),
+      base::BindOnce(&SystemMemoryPressureEvaluator::OnDiskSpaceCheckComplete,
+                     weak_ptr_factory_.GetWeakPtr()));
+}
+
+void SystemMemoryPressureEvaluator::OnDiskSpaceCheckComplete(
+    int64_t free_bytes) {
+  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
+  base::MemoryPressureListener::MemoryPressureLevel new_disk_vote =
+      base::MemoryPressureListener::MEMORY_PRESSURE_LEVEL_NONE;
+
+  const int64_t threshold_mb = kMacCriticalDiskSpacePressureThresholdMB.Get();
+  // The minimum free disk space in MB before dispatching a critical memory
+  // pressure signal.
+  const int64_t critical_disk_space_bytes = threshold_mb * kBytesPerMb;
+
+  if (free_bytes != -1 && free_bytes < critical_disk_space_bytes) {
+    new_disk_vote =
+        base::MemoryPressureListener::MEMORY_PRESSURE_LEVEL_CRITICAL;
+  }
+
+  if (disk_pressure_vote_ != new_disk_vote) {
+    disk_pressure_vote_ = new_disk_vote;
+    UpdatePressureAndManageNotifications();
+  }
+}
+
+void SystemMemoryPressureEvaluator::UpdatePressureAndManageNotifications() {
   // The OS has sent a notification that the memory pressure level has changed.
   // Go through the normal memory pressure level checking mechanism so that
   // |current_vote_| and UMA get updated to the current value.
diff --git a/components/memory_pressure/system_memory_pressure_evaluator_mac.h b/components/memory_pressure/system_memory_pressure_evaluator_mac.h
index c8cef53e..20949037 100644
--- a/components/memory_pressure/system_memory_pressure_evaluator_mac.h
+++ b/components/memory_pressure/system_memory_pressure_evaluator_mac.h
@@ -10,8 +10,10 @@
 
 #include "base/apple/scoped_cftyperef.h"
 #include "base/apple/scoped_dispatch_object.h"
+#include "base/files/file_path.h"
 #include "base/message_loop/message_pump_apple.h"
 #include "base/sequence_checker.h"
+#include "base/system/sys_info.h"
 #include "base/timer/timer.h"
 #include "components/memory_pressure/memory_pressure_voter.h"
 #include "components/memory_pressure/system_memory_pressure_evaluator.h"
@@ -50,6 +52,16 @@
   // Run |dispatch_callback| on memory pressure notifications from the OS.
   void OnMemoryPressureChanged();
 
+  // Periodically checks the amount of free disk space.
+  void CheckDiskSpace();
+
+  // Callback for the disk space check. Updates the pressure level based on the
+  // amount of free space.
+  void OnDiskSpaceCheckComplete(int64_t free_bytes);
+
+  // Updates the pressure level and manages re-notification timers.
+  void UpdatePressureAndManageNotifications();
+
   // The dispatch source that generates memory pressure change notifications.
   base::apple::ScopedDispatchObject<dispatch_source_t>
       memory_level_event_source_;
@@ -57,6 +69,19 @@
   // Timer that will re-notify with the current vote at regular interval.
   base::RepeatingTimer renotify_current_vote_timer_;
 
+  // A task runner that can be used for blocking tasks.
+  scoped_refptr<base::SequencedTaskRunner> disk_check_task_runner_;
+
+  // The timer that periodically triggers a disk space check.
+  base::RepeatingTimer disk_space_check_timer_;
+
+  // The pressure level calculated from the available disk space.
+  base::MemoryPressureListener::MemoryPressureLevel disk_pressure_vote_ =
+      base::MemoryPressureListener::MEMORY_PRESSURE_LEVEL_NONE;
+
+  // The path to the user data directory, used for the disk space check.
+  base::FilePath user_data_dir_;
+
   SEQUENCE_CHECKER(sequence_checker_);
 
   base::WeakPtrFactory<SystemMemoryPressureEvaluator> weak_ptr_factory_;