0

[self_freeze] Add function to allow compacting our own memory

We use MADV_PAGEOUT in order to swap out non-shared, non-file-backed pages that we don't need. This will be used when we go to background but are not frozen/compacted by App Freezer. This function is currently not called anywhere, so this CL should not change any behavior.

Bug: 344547190
Change-Id: Ie2bd754ba06b5cae6769221396471eb74d495522
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/5887346
Reviewed-by: Matthew Denton <mpdenton@chromium.org>
Reviewed-by: Benoit Lize <lizeb@chromium.org>
Reviewed-by: Brian Geffon <bgeffon@chromium.org>
Commit-Queue: Thiabaud Engelbrecht <thiabaud@google.com>
Cr-Commit-Position: refs/heads/main@{#1372231}
This commit is contained in:
Thiabaud Engelbrecht
2024-10-22 19:05:37 +00:00
committed by Chromium LUCI CQ
parent f8e18fc22a
commit 90d66f8045
4 changed files with 296 additions and 0 deletions

@ -4,6 +4,9 @@
#include "base/android/pre_freeze_background_memory_trimmer.h"
#include <sys/mman.h>
#include <sys/utsname.h>
#include <optional>
#include <string>
@ -15,12 +18,14 @@
#include "base/feature_list.h"
#include "base/functional/bind.h"
#include "base/logging.h"
#include "base/memory/page_size.h"
#include "base/metrics/histogram_functions.h"
#include "base/strings/strcat.h"
#include "base/task/sequenced_task_runner.h"
#include "base/task/thread_pool.h"
#include "base/task/thread_pool/thread_pool_instance.h"
#include "base/time/time.h"
#include "base/trace_event/base_tracing.h"
namespace base::android {
namespace {
@ -104,6 +109,29 @@ std::optional<uint64_t> Diff(std::optional<uint64_t> before,
return after_value < before_value ? before_value - after_value : 0;
}
bool IsMadvisePageoutSupported() {
static bool supported = []() -> bool {
#if defined(MADV_PAGEOUT)
// To determine if MADV_PAGEOUT is supported we will try calling it with an
// invalid memory area.
// madvise(2) first checks the mode first, returning -EINVAL if it's
// unknown. Next, it will always return 0 for a zero length VMA before
// validating if it's mapped.
// So, in this case, we can test for support with any page aligned address
// with a zero length.
int res =
madvise(reinterpret_cast<void*>(base::GetPageSize()), 0, MADV_PAGEOUT);
if (res < 0 && errno == -EINVAL)
return false;
PLOG_IF(ERROR, res < 0) << "Unexpected return from madvise";
if (res == 0)
return true;
#endif
return false;
}();
return supported;
}
} // namespace
BASE_FEATURE(kOnPreFreezeMemoryTrim,
@ -316,6 +344,91 @@ void PreFreezeBackgroundMemoryTrimmer::UnregisterMemoryMetricInternal(
metrics_.erase(metrics_.begin() + index);
}
// static
bool PreFreezeBackgroundMemoryTrimmer::SelfCompactionIsSupported() {
return IsMadvisePageoutSupported();
}
// static
std::optional<int64_t> PreFreezeBackgroundMemoryTrimmer::CompactSelf() {
// MADV_PAGEOUT was only added in Linux 5.4, so do nothing in earlier
// versions.
if (!SelfCompactionIsSupported()) {
return std::nullopt;
}
std::vector<debug::MappedMemoryRegion> regions;
std::string proc_maps;
if (!debug::ReadProcMaps(&proc_maps) || !ParseProcMaps(proc_maps, &regions)) {
return std::nullopt;
}
if (regions.size() == 0) {
return std::nullopt;
}
// TODO(crbug.com/344547190): This may run for a long time. Add a way to
// cancel this part-way through if we return to the foreground while this is
// running.
return CompactMemory(std::move(regions));
}
// static
std::optional<int64_t> PreFreezeBackgroundMemoryTrimmer::CompactRegion(
debug::MappedMemoryRegion region) {
#if defined(MADV_PAGEOUT)
// Skip file-backed regions
if (region.inode != 0 || region.dev_major != 0) {
return 0;
}
// Skip shared regions
if ((region.permissions & debug::MappedMemoryRegion::Permission::PRIVATE) ==
0) {
return 0;
}
TRACE_EVENT1("base", __PRETTY_FUNCTION__, "size", region.end - region.start);
int error = madvise(reinterpret_cast<void*>(region.start),
region.end - region.start, MADV_PAGEOUT);
if (error < 0) {
// We may fail on some regions, such as [vvar], or a locked region. It's
// not worth it to try to filter these all out, so we just skip them, and
// rely on metrics to verify that this is working correctly for most
// regions.
//
// EINVAL could be [vvar] or a locked region. ENOMEM would be a moved or
// unmapped region.
if (errno != EINVAL && errno != ENOMEM) {
PLOG(ERROR) << "Unexpected error from madvise.";
return std::nullopt;
}
return 0;
}
return region.end - region.start;
#else
return std::nullopt;
#endif
}
// static
std::optional<int64_t> PreFreezeBackgroundMemoryTrimmer::CompactMemory(
std::vector<debug::MappedMemoryRegion> regions) {
TRACE_EVENT1("base", __PRETTY_FUNCTION__, "count", regions.size());
int64_t total_bytes_processed = 0;
for (const auto& region : regions) {
const auto bytes_processed = CompactRegion(region);
if (!bytes_processed) {
return std::nullopt;
}
total_bytes_processed += bytes_processed.value();
}
return total_bytes_processed;
}
void PreFreezeBackgroundMemoryTrimmer::PostMetricsTasksIfModern() {
if (!SupportsModernTrim()) {
return;

@ -7,6 +7,7 @@
#include <deque>
#include "base/debug/proc_maps_linux.h"
#include "base/feature_list.h"
#include "base/functional/callback.h"
#include "base/memory/post_delayed_memory_reduction_task.h"
@ -104,6 +105,12 @@ class BASE_EXPORT PreFreezeBackgroundMemoryTrimmer {
static void UnregisterMemoryMetric(const PreFreezeMetric* metric)
LOCKS_EXCLUDED(Instance().lock_);
static bool SelfCompactionIsSupported();
// Compacts the memory for the process, and returns the number of bytes
// processed on success.
static std::optional<int64_t> CompactSelf();
static void SetSupportsModernTrimForTesting(bool is_supported);
static void ClearMetricsForTesting() LOCKS_EXCLUDED(lock_);
size_t GetNumberOfPendingBackgroundTasksForTesting() const
@ -114,6 +121,8 @@ class BASE_EXPORT PreFreezeBackgroundMemoryTrimmer {
static void OnPreFreezeForTesting() LOCKS_EXCLUDED(lock_) { OnPreFreeze(); }
static std::optional<int64_t> CompactRegion(debug::MappedMemoryRegion region);
// Called when Chrome is about to be frozen. Runs as many delayed tasks as
// possible immediately, before we are frozen.
static void OnPreFreeze() LOCKS_EXCLUDED(lock_);
@ -128,6 +137,8 @@ class BASE_EXPORT PreFreezeBackgroundMemoryTrimmer {
JNIEnv* env);
friend class base::android::MemoryPurgeManagerAndroid;
friend class base::OneShotDelayedBackgroundTimer;
friend class PreFreezeBackgroundMemoryTrimmerTest;
friend class PreFreezeSelfCompactionTest;
// We use our own implementation here, based on |PostCancelableDelayedTask|,
// rather than relying on something like |base::OneShotTimer|, since
@ -168,6 +179,9 @@ class BASE_EXPORT PreFreezeBackgroundMemoryTrimmer {
PreFreezeBackgroundMemoryTrimmer();
static std::optional<int64_t> CompactMemory(
std::vector<debug::MappedMemoryRegion> regions);
void RegisterMemoryMetricInternal(const PreFreezeMetric* metric)
EXCLUSIVE_LOCKS_REQUIRED(lock_);

@ -4,8 +4,15 @@
#include "base/android/pre_freeze_background_memory_trimmer.h"
#include <sys/fcntl.h>
#include <sys/mman.h>
#include <sys/utsname.h>
#include <optional>
#include "base/files/file_util.h"
#include "base/files/scoped_temp_file.h"
#include "base/memory/page_size.h"
#include "base/task/thread_pool.h"
#include "base/test/scoped_feature_list.h"
#include "base/test/task_environment.h"
@ -48,6 +55,44 @@ class MockMetric : public PreFreezeBackgroundMemoryTrimmer::PreFreezeMetric {
size_t MockMetric::count_ = 0;
std::optional<const debug::MappedMemoryRegion> GetMappedMemoryRegion(
uintptr_t addr) {
std::vector<debug::MappedMemoryRegion> regions;
std::string proc_maps;
if (!debug::ReadProcMaps(&proc_maps) || !ParseProcMaps(proc_maps, &regions)) {
return std::nullopt;
}
for (const auto& region : regions) {
if (region.start == addr) {
return region;
}
}
return std::nullopt;
}
std::optional<const debug::MappedMemoryRegion> GetMappedMemoryRegion(
void* addr) {
return GetMappedMemoryRegion(reinterpret_cast<uintptr_t>(addr));
}
size_t CountResidentPagesInRange(void* addr, size_t size) {
DCHECK((reinterpret_cast<uintptr_t>(addr) & (base::GetPageSize() - 1)) == 0);
DCHECK(size % base::GetPageSize() == 0);
std::vector<unsigned char> pages(size / base::GetPageSize());
mincore(addr, size, &pages[0]);
size_t tmp = 0;
for (const auto& v : pages) {
tmp += v & 0x01;
}
return tmp;
}
} // namespace
class PreFreezeBackgroundMemoryTrimmerTest : public testing::Test {
@ -603,4 +648,119 @@ TEST_F(PreFreezeBackgroundMemoryTrimmerTest, TimerBoolTaskRunFromPreFreeze) {
EXPECT_EQ(called_task_type.value(), MemoryReductionTaskContext::kProactive);
}
TEST(PreFreezeSelfCompactionTest, Simple) {
// MADV_PAGEOUT is only supported starting from Linux 5.4. So, on devices
// don't support it, we bail out early. This is a known problem on some 32
// bit devices.
if (!PreFreezeBackgroundMemoryTrimmer::SelfCompactionIsSupported()) {
GTEST_SKIP() << "No kernel support";
}
const size_t kPageSize = base::GetPageSize();
const size_t kNumPages = 24;
const size_t size = kNumPages * kPageSize;
void* addr = nullptr;
addr = mmap(nullptr, size, PROT_WRITE | PROT_READ, MAP_PRIVATE | MAP_ANON, -1,
0);
ASSERT_NE(addr, MAP_FAILED);
// we touch the memory here to dirty it, so that it is definitely resident.
memset((void*)addr, 1, size);
EXPECT_EQ(CountResidentPagesInRange(addr, size), kNumPages);
const auto region = GetMappedMemoryRegion(addr);
ASSERT_TRUE(region);
const auto result =
PreFreezeBackgroundMemoryTrimmer::CompactRegion(std::move(*region));
ASSERT_EQ(result, size);
EXPECT_EQ(CountResidentPagesInRange(addr, size), 0u);
munmap(addr, size);
}
TEST(PreFreezeSelfCompactionTest, File) {
// MADV_PAGEOUT is only supported starting from Linux 5.4. So, on devices
// don't support it, we bail out early. This is a known problem on some 32
// bit devices.
if (!PreFreezeBackgroundMemoryTrimmer::SelfCompactionIsSupported()) {
GTEST_SKIP() << "No kernel support";
}
const size_t kPageSize = base::GetPageSize();
const size_t kNumPages = 2;
const size_t size = kNumPages * kPageSize;
ScopedTempFile file;
ASSERT_TRUE(file.Create());
int fd = open(file.path().value().c_str(), O_RDWR);
ASSERT_NE(fd, -1);
ASSERT_TRUE(base::WriteFile(file.path(), std::string(size, 1)));
void* addr = nullptr;
addr = mmap(nullptr, size, PROT_WRITE | PROT_READ, MAP_PRIVATE, fd, 0);
ASSERT_NE(addr, MAP_FAILED);
// we touch the memory here to dirty it, so that it is definitely resident.
memset((void*)addr, 2, size);
EXPECT_EQ(CountResidentPagesInRange(addr, size), kNumPages);
const auto region = GetMappedMemoryRegion(addr);
ASSERT_TRUE(region);
const auto result =
PreFreezeBackgroundMemoryTrimmer::CompactRegion(std::move(*region));
ASSERT_EQ(result, 0);
EXPECT_EQ(CountResidentPagesInRange(addr, size), kNumPages);
munmap(addr, size);
}
TEST(PreFreezeSelfCompactionTest, Locked) {
// MADV_PAGEOUT is only supported starting from Linux 5.4. So, on devices
// don't support it, we bail out early. This is a known problem on some 32
// bit devices.
if (!PreFreezeBackgroundMemoryTrimmer::SelfCompactionIsSupported()) {
GTEST_SKIP() << "No kernel support";
}
const size_t kPageSize = base::GetPageSize();
// We use a small number of pages here because Android has a low limit on
// the max locked size allowed (~64 KiB on many devices).
const size_t kNumPages = 2;
const size_t size = kNumPages * kPageSize;
void* addr = nullptr;
addr = mmap(nullptr, size, PROT_WRITE | PROT_READ, MAP_PRIVATE | MAP_ANON, -1,
0);
ASSERT_NE(addr, MAP_FAILED);
ASSERT_EQ(mlock(addr, size), 0);
// we touch the memory here to dirty it, so that it is definitely resident.
memset((void*)addr, 1, size);
EXPECT_EQ(CountResidentPagesInRange(addr, size), kNumPages);
const auto region = GetMappedMemoryRegion(addr);
ASSERT_TRUE(region);
const auto result =
PreFreezeBackgroundMemoryTrimmer::CompactRegion(std::move(*region));
ASSERT_EQ(result, 0);
EXPECT_EQ(CountResidentPagesInRange(addr, size), kNumPages);
munlock(addr, size);
munmap(addr, size);
}
} // namespace base::android

@ -250,6 +250,15 @@ ResultExpr BaselinePolicyAndroid::EvaluateSyscall(int sysno) const {
return RestrictAndroidIoctl(options_.allow_userfaultfd_ioctls);
}
#if defined(MADV_PAGEOUT)
if (sysno == __NR_madvise) {
// Allow MADV_PAGEOUT
const Arg<int> advice(2);
return If(advice == MADV_PAGEOUT, Allow())
.Else(BaselinePolicy::EvaluateSyscall(sysno));
}
#endif
// Ptrace is allowed so the crash reporter can fork in a renderer
// and then ptrace the parent. https://crbug.com/933418
if (sysno == __NR_ptrace) {