Skip to content

[scudo] Add new fast purge option.#175266

Merged
cferris1000 merged 3 commits intollvm:mainfrom
cferris1000:force
Feb 11, 2026
Merged

[scudo] Add new fast purge option.#175266
cferris1000 merged 3 commits intollvm:mainfrom
cferris1000:force

Conversation

@cferris1000
Copy link
Contributor

This adds a new option to do a faster of a purge.

When doing a release to OS due to a purge call, if another thread is also doing a release, the call can be blocked while that operation concludes. In some cases, code wants a fast version that releases as fast as possible and the call will not block.

For example, on Android, when destroying a Bitmap a purge occurs to save memory. But this can cause some jank if the purge takes too long.

In the future, I envision that this option will also do a calculation to stop purging after some cutoff value to avoid being blocked in this call for too long.

This adds a new option to do a faster of a purge.

When doing a release to OS due to a purge call, if another thread
is also doing a release, the call can be blocked while that
operation concludes. In some cases, code wants a fast version
that releases as fast as possible and the call will not block.

For example, on Android, when destroying a Bitmap a purge occurs
to save memory. But this can cause some jank if the purge
takes too long.

In the future, I envision that this option will also do a calculation
to stop purging after some cutoff value to avoid being blocked in
this call for too long.
@llvmbot
Copy link
Member

llvmbot commented Jan 9, 2026

@llvm/pr-subscribers-compiler-rt-sanitizer

Author: Christopher Ferris (cferris1000)

Changes

This adds a new option to do a faster of a purge.

When doing a release to OS due to a purge call, if another thread is also doing a release, the call can be blocked while that operation concludes. In some cases, code wants a fast version that releases as fast as possible and the call will not block.

For example, on Android, when destroying a Bitmap a purge occurs to save memory. But this can cause some jank if the purge takes too long.

In the future, I envision that this option will also do a calculation to stop purging after some cutoff value to avoid being blocked in this call for too long.


Full diff: https://github.com/llvm/llvm-project/pull/175266.diff

7 Files Affected:

  • (modified) compiler-rt/lib/scudo/standalone/common.h (+2)
  • (modified) compiler-rt/lib/scudo/standalone/include/scudo/interface.h (+4)
  • (modified) compiler-rt/lib/scudo/standalone/primary32.h (+11-2)
  • (modified) compiler-rt/lib/scudo/standalone/primary64.h (+11-2)
  • (modified) compiler-rt/lib/scudo/standalone/secondary.h (+13-4)
  • (modified) compiler-rt/lib/scudo/standalone/tests/combined_test.cpp (+11)
  • (modified) compiler-rt/lib/scudo/standalone/wrappers_c.inc (+3)
diff --git a/compiler-rt/lib/scudo/standalone/common.h b/compiler-rt/lib/scudo/standalone/common.h
index 9097532f750d9..20f8c1d3fc65c 100644
--- a/compiler-rt/lib/scudo/standalone/common.h
+++ b/compiler-rt/lib/scudo/standalone/common.h
@@ -251,6 +251,8 @@ enum class ReleaseToOS : u8 {
   Force,  // Force release pages to the OS, but avoid cases that take too long.
   ForceAll, // Force release every page possible regardless of how long it will
             // take.
+  ForceFast, // Force release pages to the OS, but do it quickly and skip any
+             // cases where a lock is held by another thread.
 };
 
 constexpr unsigned char PatternFillByte = 0xAB;
diff --git a/compiler-rt/lib/scudo/standalone/include/scudo/interface.h b/compiler-rt/lib/scudo/standalone/include/scudo/interface.h
index a2dedea910cc0..9f2b93891999d 100644
--- a/compiler-rt/lib/scudo/standalone/include/scudo/interface.h
+++ b/compiler-rt/lib/scudo/standalone/include/scudo/interface.h
@@ -135,6 +135,10 @@ size_t __scudo_get_ring_buffer_size(void);
 #define M_PURGE_ALL -104
 #endif
 
+#ifndef M_PURGE_FAST
+#define M_PURGE_FAST -105
+#endif
+
 // Tune the allocator's choice of memory tags to make it more likely that
 // a certain class of memory errors will be detected. The value argument should
 // be one of the M_MEMTAG_TUNING_* constants below.
diff --git a/compiler-rt/lib/scudo/standalone/primary32.h b/compiler-rt/lib/scudo/standalone/primary32.h
index 4385f4aa58450..037fa3d7a7dcb 100644
--- a/compiler-rt/lib/scudo/standalone/primary32.h
+++ b/compiler-rt/lib/scudo/standalone/primary32.h
@@ -519,8 +519,17 @@ uptr SizeClassAllocator32<Config>::releaseToOS(ReleaseToOS ReleaseType) {
     if (I == SizeClassMap::BatchClassId)
       continue;
     SizeClassInfo *Sci = getSizeClassInfo(I);
-    ScopedLock L(Sci->Mutex);
-    TotalReleasedBytes += releaseToOSMaybe(Sci, I, ReleaseType);
+    if (ReleaseType == ReleaseToOS::ForceFast) {
+      // Never wait for the lock, always move on if there is already
+      // a release operation in progress.
+      if (Sci->Mutex.tryLock()) {
+        TotalReleasedBytes += releaseToOSMaybe(Sci, I, ReleaseType);
+        Sci->Mutex.unlock();
+      }
+    } else {
+      ScopedLock L(Sci->Mutex);
+      TotalReleasedBytes += releaseToOSMaybe(Sci, I, ReleaseType);
+    }
   }
   return TotalReleasedBytes;
 }
diff --git a/compiler-rt/lib/scudo/standalone/primary64.h b/compiler-rt/lib/scudo/standalone/primary64.h
index a844d7dcbfd63..0744e36c00253 100644
--- a/compiler-rt/lib/scudo/standalone/primary64.h
+++ b/compiler-rt/lib/scudo/standalone/primary64.h
@@ -1329,8 +1329,17 @@ uptr SizeClassAllocator64<Config>::releaseToOS(ReleaseToOS ReleaseType) {
     if (I == SizeClassMap::BatchClassId)
       continue;
     RegionInfo *Region = getRegionInfo(I);
-    ScopedLock L(Region->MMLock);
-    TotalReleasedBytes += releaseToOSMaybe(Region, I, ReleaseType);
+    if (ReleaseType == ReleaseToOS::ForceFast) {
+      // Never wait for the lock, always move on if there is already
+      // a release operation in progress.
+      if (Region->MMLock.tryLock()) {
+        TotalReleasedBytes += releaseToOSMaybe(Region, I, ReleaseType);
+        Region->MMLock.unlock();
+      }
+    } else {
+      ScopedLock L(Region->MMLock);
+      TotalReleasedBytes += releaseToOSMaybe(Region, I, ReleaseType);
+    }
   }
   return TotalReleasedBytes;
 }
diff --git a/compiler-rt/lib/scudo/standalone/secondary.h b/compiler-rt/lib/scudo/standalone/secondary.h
index 04e33c04baa34..f8dba33708f4b 100644
--- a/compiler-rt/lib/scudo/standalone/secondary.h
+++ b/compiler-rt/lib/scudo/standalone/secondary.h
@@ -515,10 +515,19 @@ class MapAllocatorCache {
   void releaseToOS([[maybe_unused]] ReleaseToOS ReleaseType) EXCLUDES(Mutex) {
     SCUDO_SCOPED_TRACE(GetSecondaryReleaseToOSTraceName(ReleaseType));
 
-    // Since this is a request to release everything, always wait for the
-    // lock so that we guarantee all entries are released after this call.
-    ScopedLock L(Mutex);
-    releaseOlderThan(UINT64_MAX);
+    if (ReleaseType == ReleaseToOS::ForceFast) {
+      // Never wait for the lock, always move on if there is already
+      // a release operation in progress.
+      if (Mutex.tryLock()) {
+        releaseOlderThan(UINT64_MAX);
+        Mutex.unlock();
+      }
+    } else {
+      // Since this is a request to release everything, always wait for the
+      // lock so that we guarantee all entries are released after this call.
+      ScopedLock L(Mutex);
+      releaseOlderThan(UINT64_MAX);
+    }
   }
 
   void disableMemoryTagging() EXCLUDES(Mutex) {
diff --git a/compiler-rt/lib/scudo/standalone/tests/combined_test.cpp b/compiler-rt/lib/scudo/standalone/tests/combined_test.cpp
index 01fadcf3425d6..583529c652b57 100644
--- a/compiler-rt/lib/scudo/standalone/tests/combined_test.cpp
+++ b/compiler-rt/lib/scudo/standalone/tests/combined_test.cpp
@@ -726,6 +726,17 @@ SCUDO_TYPED_TEST(ScudoCombinedTest, ThreadedCombined) {
   Allocator->releaseToOS(scudo::ReleaseToOS::Force);
 }
 
+SCUDO_TYPED_TEST(ScudoCombinedTest, ForceFast) {
+  auto *Allocator = this->Allocator.get();
+
+  // Simple smoke test to verify that ForceFast does crash.
+  void *P = Allocator->allocate(2048, Origin);
+  memset(P, 0xff, 2048);
+  Allocator->deallocate(P, Origin);
+
+  Allocator->releaseToOS(scudo::ReleaseToOS::ForceFast);
+}
+
 // Test that multiple instantiations of the allocator have not messed up the
 // process's signal handlers (GWP-ASan used to do this).
 TEST(ScudoCombinedDeathTest, SKIP_ON_FUCHSIA(testSEGV)) {
diff --git a/compiler-rt/lib/scudo/standalone/wrappers_c.inc b/compiler-rt/lib/scudo/standalone/wrappers_c.inc
index 59f3fb0962f8b..be6a6cf024f42 100644
--- a/compiler-rt/lib/scudo/standalone/wrappers_c.inc
+++ b/compiler-rt/lib/scudo/standalone/wrappers_c.inc
@@ -265,6 +265,9 @@ INTERFACE WEAK int SCUDO_PREFIX(mallopt)(int param, int value) {
   } else if (param == M_PURGE) {
     SCUDO_ALLOCATOR.releaseToOS(scudo::ReleaseToOS::Force);
     return 1;
+  } else if (param == M_PURGE_FAST) {
+    SCUDO_ALLOCATOR.releaseToOS(scudo::ReleaseToOS::ForceFast);
+    return 1;
   } else if (param == M_PURGE_ALL) {
     SCUDO_ALLOCATOR.releaseToOS(scudo::ReleaseToOS::ForceAll);
     return 1;

@ajordanr-google
Copy link
Contributor

In some cases, code wants a fast version that releases as fast as possible and the call will not block.

To clarify, you mean block on trying to acquire a lock? Not on blocking while trying to conduct the purge.

In the future, I envision that this option will also do a calculation to stop purging after some cutoff value to avoid being blocked in this call for too long.

Clarifying, this is about FastPurge not Purge? Purge I would have thought already does this, but may wait on locks, but the description of the PR makes it a little confusing.

@cferris1000
Copy link
Contributor Author

In some cases, code wants a fast version that releases as fast as possible and the call will not block.

To clarify, you mean block on trying to acquire a lock? Not on blocking while trying to conduct the purge.

Yes, the purge operation can take a bit of time, so we don't want this version to block waiting from some other thread doing a purge.

In the future, I envision that this option will also do a calculation to stop purging after some cutoff value to avoid being blocked in this call for too long.

Clarifying, this is about FastPurge not Purge? Purge I would have thought already does this, but may wait on locks, but the description of the PR makes it a little confusing.

Yes, this is for FastPurge not the normal purge. And the normal purge doesn't do a cut-off because it's normally relatively fast. However, if you wind up waiting for another purge to complete, it can take a bit more time.

@cferris1000 cferris1000 merged commit a816f92 into llvm:main Feb 11, 2026
10 checks passed
kevinwkt pushed a commit to kevinwkt/llvm-project that referenced this pull request Feb 16, 2026
This adds a new option to do a faster of a purge.

When doing a release to OS due to a purge call, if another thread is
also doing a release, the call can be blocked while that operation
concludes. In some cases, code wants a fast version that releases as
fast as possible and the call will not block.

For example, on Android, when destroying a Bitmap a purge occurs to save
memory. But this can cause some jank if the purge takes too long.

In the future, I envision that this option will also do a calculation to
stop purging after some cutoff value to avoid being blocked in this call
for too long.
manasij7479 pushed a commit to manasij7479/llvm-project that referenced this pull request Feb 18, 2026
This adds a new option to do a faster of a purge.

When doing a release to OS due to a purge call, if another thread is
also doing a release, the call can be blocked while that operation
concludes. In some cases, code wants a fast version that releases as
fast as possible and the call will not block.

For example, on Android, when destroying a Bitmap a purge occurs to save
memory. But this can cause some jank if the purge takes too long.

In the future, I envision that this option will also do a calculation to
stop purging after some cutoff value to avoid being blocked in this call
for too long.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants