[scudo] Track maximum resident memory in cache (#219036)

Add tracking for the resident memory in MapAllocatorCache. Unreleased
cache entries where Time != 0 are assumed to be resident, while entries
that have passed through releaseOlderThan where Time == 0 have had their
physical pages discarded.

Tracking is performed by updating CurrentResidentBytes on store, remove,
and release operations, recoding the peak in maxResidentBytes and
reporting it in getStats.

GitOrigin-RevId: 5964066e909c5c443bd2f48a88dc54f263a4d47e
diff --git a/lib/scudo/standalone/secondary.h b/lib/scudo/standalone/secondary.h
index 16e94f0..5750636 100644
--- a/lib/scudo/standalone/secondary.h
+++ b/lib/scudo/standalone/secondary.h
@@ -142,6 +142,9 @@
     return true;
   }
 
+  uptr getMaxResidentBytesTestOnly() const { return 0; }
+  uptr getCurrentResidentBytesTestOnly() const { return 0; }
+
   void getStats(UNUSED ScopedString *Str) {
     Str->append("Secondary Cache Disabled\n");
   }
@@ -231,11 +234,12 @@
     Str->append(
         "Stats: MapAllocatorCache: EntriesCount: %zu, "
         "MaxEntriesCount: %u, MaxEntrySize: %zu, ReleaseToOsSkips: "
-        "%zu, ReleaseToOsIntervalMs = %d, Unmapped due to eviction: %u, ",
+        "%zu, ReleaseToOsIntervalMs = %d, Unmapped due to eviction: %u, "
+        "MaxResidentBytes: %zu, CurrentResidentBytes: %zu\n",
         LRUEntries.size(), atomic_load_relaxed(&MaxEntriesCount),
         atomic_load_relaxed(&MaxEntrySize),
         atomic_load_relaxed(&ReleaseToOsSkips), Interval >= 0 ? Interval : -1,
-        EvictedCount);
+        EvictedCount, MaxResidentBytes, CurrentResidentBytes);
     Str->append("Stats: CacheRetrievalStats: SuccessRate: %u/%u "
                 "(%zu.%02zu%%)\n",
                 SuccessfulRetrieves, CallsToRetrieve, Integral, Fractional);
@@ -565,6 +569,16 @@
 
   void enable() NO_THREAD_SAFETY_ANALYSIS { Mutex.unlock(); }
 
+  uptr getMaxResidentBytesTestOnly() {
+    ScopedLock L(Mutex);
+    return MaxResidentBytes;
+  }
+
+  uptr getCurrentResidentBytesTestOnly() {
+    ScopedLock L(Mutex);
+    return CurrentResidentBytes;
+  }
+
   void unmapTestOnly() { empty(); }
 
   void releaseOlderThanTestOnly(u64 ReleaseTime) {
@@ -581,6 +595,11 @@
     LRUEntries.push_front(AvailEntry);
     if (OldestPresentEntry == nullptr && AvailEntry->Time != 0)
       OldestPresentEntry = AvailEntry;
+    if (AvailEntry->Time != 0) {
+      CurrentResidentBytes += Entry.CommitSize;
+      if (CurrentResidentBytes > MaxResidentBytes)
+        MaxResidentBytes = CurrentResidentBytes;
+    }
   }
 
   void remove(CachedBlock *Entry) REQUIRES(Mutex) {
@@ -590,6 +609,8 @@
       DCHECK(OldestPresentEntry == nullptr || OldestPresentEntry->Time != 0);
     }
     LRUEntries.remove(Entry);
+    if (Entry->Time != 0)
+      CurrentResidentBytes -= Entry->CommitSize;
     Entry->invalidate();
     AvailEntries.push_front(Entry);
   }
@@ -604,6 +625,7 @@
         MapInfo[N++] = Entry.MemMap;
       LRUEntries.clear();
       OldestPresentEntry = nullptr;
+      CurrentResidentBytes = 0;
     }
     for (uptr I = 0; I < N; I++) {
       MemMapT &MemMap = MapInfo[I];
@@ -638,6 +660,7 @@
 
       Entry->MemMap.releaseAndZeroPagesToOS(Entry->CommitBase,
                                             Entry->CommitSize);
+      CurrentResidentBytes -= Entry->CommitSize;
       Entry->Time = 0;
     }
     OldestPresentEntry = nullptr;
@@ -651,6 +674,8 @@
   u32 CallsToRetrieve GUARDED_BY(Mutex) = 0;
   u32 SuccessfulRetrieves GUARDED_BY(Mutex) = 0;
   u32 EvictedCount GUARDED_BY(Mutex) = 0;
+  uptr CurrentResidentBytes GUARDED_BY(Mutex) = 0;
+  uptr MaxResidentBytes GUARDED_BY(Mutex) = 0;
   atomic_uptr ReleaseToOsSkips = {};
 
   CachedBlock Entries[Config::getEntriesArraySize()] GUARDED_BY(Mutex) = {};
@@ -736,6 +761,14 @@
 
   void unmapTestOnly() { Cache.unmapTestOnly(); }
 
+  uptr getMaxResidentBytesTestOnly() {
+    return Cache.getMaxResidentBytesTestOnly();
+  }
+
+  uptr getCurrentResidentBytesTestOnly() {
+    return Cache.getCurrentResidentBytesTestOnly();
+  }
+
   void getStats(ScopedString *Str);
 
 private:
diff --git a/lib/scudo/standalone/tests/secondary_test.cpp b/lib/scudo/standalone/tests/secondary_test.cpp
index a3cb493..8e9d78c 100644
--- a/lib/scudo/standalone/tests/secondary_test.cpp
+++ b/lib/scudo/standalone/tests/secondary_test.cpp
@@ -680,3 +680,47 @@
     EXPECT_EQ(*reinterpret_cast<scudo::u32 *>(Info.MemMaps[I].getBase()), 0U);
   }
 }
+
+TEST(ScudoSecondaryTest, AllocatorCacheMaxResidentBytes) {
+  CacheInfoType<TestCacheConfig> Info;
+
+  Info.Cache->setOption(scudo::Option::ReleaseInterval, -1);
+  Info.Cache->setOption(scudo::Option::MaxCacheEntriesCount, 10);
+  Info.Cache->setOption(scudo::Option::MaxCacheEntrySize, 1024 * 1024);
+
+  EXPECT_EQ(Info.Cache->getCurrentResidentBytesTestOnly(), 0U);
+  EXPECT_EQ(Info.Cache->getMaxResidentBytesTestOnly(), 0U);
+
+  Info.MemMaps.emplace_back(Info.allocate(1024));
+  const scudo::uptr Size1 = Info.MemMaps[0].getCapacity();
+  EXPECT_NE(0U, Size1);
+  Info.storeMemMap(Info.MemMaps[0]);
+
+  EXPECT_EQ(Info.Cache->getCurrentResidentBytesTestOnly(), Size1);
+  EXPECT_EQ(Info.Cache->getMaxResidentBytesTestOnly(), Size1);
+
+  Info.MemMaps.emplace_back(Info.allocate(1024));
+  const scudo::uptr Size2 = Info.MemMaps[1].getCapacity();
+  EXPECT_NE(0U, Size2);
+  Info.storeMemMap(Info.MemMaps[1]);
+
+  EXPECT_EQ(Info.Cache->getCurrentResidentBytesTestOnly(), Size1 + Size2);
+  EXPECT_EQ(Info.Cache->getMaxResidentBytesTestOnly(), Size1 + Size2);
+
+  const scudo::uptr PeakBytes = Size1 + Size2;
+
+  // Releasing pages should drop CurrentResidentBytes to 0, while
+  // MaxResidentBytes stays at peak
+  Info.Cache->releaseOlderThanTestOnly(UINT64_MAX);
+  EXPECT_EQ(Info.Cache->getCurrentResidentBytesTestOnly(), 0U);
+  EXPECT_EQ(Info.Cache->getMaxResidentBytesTestOnly(), PeakBytes);
+
+  // Store a third map to verify CurrentResidentBytes increases again
+  Info.MemMaps.emplace_back(Info.allocate(1024));
+  const scudo::uptr Size3 = Info.MemMaps[2].getCapacity();
+  EXPECT_NE(0U, Size3);
+  Info.storeMemMap(Info.MemMaps[2]);
+
+  EXPECT_EQ(Info.Cache->getCurrentResidentBytesTestOnly(), Size3);
+  EXPECT_EQ(Info.Cache->getMaxResidentBytesTestOnly(), PeakBytes);
+}