[scudo] Add new fast purge option.#175266
Conversation
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.
|
@llvm/pr-subscribers-compiler-rt-sanitizer Author: Christopher Ferris (cferris1000) ChangesThis 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:
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;
|
To clarify, you mean block on trying to acquire a lock? Not on blocking while trying to conduct the purge.
Clarifying, this is about |
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.
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. |
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.
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.