Commit b26a6063 authored by Thiabaud Engelbrecht's avatar Thiabaud Engelbrecht Committed by Commit Bot

[discardable] Periodic purging of unlocked discardable memory

This CL lets us schedule a purge of unlocked discardable memory from the
foreground tab to run every minute.

This is hidden behind a feature which is disabled by default, so there
should be no changes to functionality.

Bug: 1109209
Change-Id: Icb2474b1026e2283ccc772cd3a602d46ef628dc8
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2370655Reviewed-by: default avatarPeng Huang <penghuang@chromium.org>
Reviewed-by: default avatarKentaro Hara <haraken@chromium.org>
Reviewed-by: default avatarBenoit L <lizeb@chromium.org>
Commit-Queue: Thiabaud Engelbrecht <thiabaud@google.com>
Cr-Commit-Position: refs/heads/master@{#803848}
parent e6c7b701
......@@ -9,7 +9,6 @@
#include "base/atomic_sequence_num.h"
#include "base/bind.h"
#include "base/feature_list.h"
#include "base/macros.h"
#include "base/memory/discardable_memory.h"
#include "base/memory/discardable_shared_memory.h"
......@@ -27,10 +26,6 @@
#include "components/crash/core/common/crash_key.h"
namespace discardable_memory {
namespace {
// Global atomic to generate unique discardable shared memory IDs.
base::AtomicSequenceNumber g_next_discardable_shared_memory_id;
// This controls whether unlocked memory is released when |ReleaseFreeMemory| is
// called. Enabling this causes |ReleaseFreeMemory| to release all
......@@ -39,6 +34,19 @@ base::AtomicSequenceNumber g_next_discardable_shared_memory_id;
const base::Feature kPurgeUnlockedMemory{"PurgeUnlockedMemory",
base::FEATURE_DISABLED_BY_DEFAULT};
// This controls whether unlocked memory is periodically purged from the
// foreground process. Enabling this causes a task to be scheduled at regular
// intervals to purge unlocked memory that hasn't been touched in a while. This
// task is stopped if no discardable memory is left, and restarted at the next
// allocation.
const base::Feature kSchedulePeriodicPurge{"SchedulePeriodicPurge",
base::FEATURE_DISABLED_BY_DEFAULT};
namespace {
// Global atomic to generate unique discardable shared memory IDs.
base::AtomicSequenceNumber g_next_discardable_shared_memory_id;
size_t GetDefaultAllocationSize() {
const size_t kOneMegabyteInBytes = 1024 * 1024;
......@@ -81,11 +89,14 @@ void DeletedDiscardableSharedMemoryOnIO(
} // namespace
constexpr base::TimeDelta
ClientDiscardableSharedMemoryManager::kMinAgeForScheduledPurge;
ClientDiscardableSharedMemoryManager::DiscardableMemoryImpl::
DiscardableMemoryImpl(
ClientDiscardableSharedMemoryManager* manager,
std::unique_ptr<DiscardableSharedMemoryHeap::Span> span)
: manager_(manager), span_(std::move(span)), is_locked_(true) {
: manager_(manager), span_(std::move(span)) {
DCHECK_NE(manager, nullptr);
}
......@@ -93,10 +104,10 @@ ClientDiscardableSharedMemoryManager::DiscardableMemoryImpl::
~DiscardableMemoryImpl() {
base::AutoLock lock(manager_->GetLock());
if (!span_) {
DCHECK(!is_locked_);
DCHECK(!is_locked());
return;
}
if (is_locked_)
if (is_locked())
manager_->UnlockSpan(span_.get());
manager_->ReleaseMemory(this, std::move(span_));
......@@ -104,30 +115,37 @@ ClientDiscardableSharedMemoryManager::DiscardableMemoryImpl::
bool ClientDiscardableSharedMemoryManager::DiscardableMemoryImpl::Lock() {
base::AutoLock lock(manager_->GetLock());
DCHECK(!is_locked_);
DCHECK(!is_locked());
if (span_ && manager_->LockSpan(span_.get()))
is_locked_ = true;
last_locked_ = base::TimeTicks();
UMA_HISTOGRAM_BOOLEAN("Memory.Discardable.LockingSuccess", is_locked_);
bool locked = is_locked();
UMA_HISTOGRAM_BOOLEAN("Memory.Discardable.LockingSuccess", locked);
return is_locked_;
return locked;
}
void ClientDiscardableSharedMemoryManager::DiscardableMemoryImpl::Unlock() {
base::AutoLock lock(manager_->GetLock());
DCHECK(is_locked_);
DCHECK(is_locked());
DCHECK(span_);
manager_->UnlockSpan(span_.get());
is_locked_ = false;
last_locked_ = base::TimeTicks::Now();
}
std::unique_ptr<DiscardableSharedMemoryHeap::Span>
ClientDiscardableSharedMemoryManager::DiscardableMemoryImpl::Purge() {
ClientDiscardableSharedMemoryManager::DiscardableMemoryImpl::Purge(
base::TimeTicks min_ticks) {
DCHECK(span_);
if (is_locked_)
if (is_locked())
return nullptr;
if (last_locked_ > min_ticks)
return nullptr;
return std::move(span_);
}
......@@ -136,18 +154,23 @@ void* ClientDiscardableSharedMemoryManager::DiscardableMemoryImpl::data()
#if DCHECK_IS_ON()
{
base::AutoLock lock(manager_->GetLock());
DCHECK(is_locked_);
DCHECK(is_locked());
}
#endif
return reinterpret_cast<void*>(span_->start() * base::GetPageSize());
}
bool ClientDiscardableSharedMemoryManager::DiscardableMemoryImpl::is_locked()
const {
return last_locked_.is_null();
}
void ClientDiscardableSharedMemoryManager::DiscardableMemoryImpl::
DiscardForTesting() {
#if DCHECK_IS_ON()
{
base::AutoLock lock(manager_->GetLock());
DCHECK(!is_locked_);
DCHECK(!is_locked());
}
#endif
span_->shared_memory()->Purge(base::Time::Now());
......@@ -162,8 +185,10 @@ base::trace_event::MemoryAllocatorDump* ClientDiscardableSharedMemoryManager::
ClientDiscardableSharedMemoryManager::ClientDiscardableSharedMemoryManager(
mojo::PendingRemote<mojom::DiscardableSharedMemoryManager> manager,
scoped_refptr<base::SingleThreadTaskRunner> io_task_runner)
scoped_refptr<base::SingleThreadTaskRunner> io_task_runner,
scoped_refptr<base::SingleThreadTaskRunner> periodic_purge_task_runner)
: heap_(std::make_unique<DiscardableSharedMemoryHeap>()),
periodic_purge_task_runner_(std::move(periodic_purge_task_runner)),
io_task_runner_(std::move(io_task_runner)),
manager_mojo_(std::make_unique<
mojo::Remote<mojom::DiscardableSharedMemoryManager>>()) {
......@@ -176,8 +201,10 @@ ClientDiscardableSharedMemoryManager::ClientDiscardableSharedMemoryManager(
}
ClientDiscardableSharedMemoryManager::ClientDiscardableSharedMemoryManager(
scoped_refptr<base::SingleThreadTaskRunner> io_task_runner)
scoped_refptr<base::SingleThreadTaskRunner> io_task_runner,
scoped_refptr<base::SingleThreadTaskRunner> periodic_purge_task_runner)
: heap_(std::make_unique<DiscardableSharedMemoryHeap>()),
periodic_purge_task_runner_(std::move(periodic_purge_task_runner)),
io_task_runner_(std::move(io_task_runner)) {}
ClientDiscardableSharedMemoryManager::~ClientDiscardableSharedMemoryManager() {
......@@ -205,6 +232,9 @@ ClientDiscardableSharedMemoryManager::AllocateLockedDiscardableMemory(
size_t size) {
base::AutoLock lock(lock_);
if (timer_ == nullptr && periodic_purge_task_runner_)
StartScheduledPurging(periodic_purge_task_runner_);
DCHECK_NE(size, 0u);
auto size_in_kb = static_cast<base::HistogramBase::Sample>(size / 1024);
......@@ -324,6 +354,26 @@ ClientDiscardableSharedMemoryManager::AllocateLockedDiscardableMemory(
return std::move(discardable_memory);
}
void ClientDiscardableSharedMemoryManager::StartScheduledPurging(
scoped_refptr<base::SequencedTaskRunner> task_runner) {
DCHECK(task_runner);
if (base::FeatureList::IsEnabled(kSchedulePeriodicPurge)) {
// The expected cost of purging should be very small (< 1ms), so it can be
// scheduled frequently. However, we don't purge memory that has been
// touched recently (see: |BackgroundPurge| and |kMinAgeForScheduledPurge|),
// so there is no benefit to running this more than once per minute.
constexpr base::TimeDelta kInterval = base::TimeDelta::FromMinutes(1);
timer_ = std::make_unique<base::RepeatingTimer>();
timer_->SetTaskRunner(task_runner);
timer_->Start(FROM_HERE, kInterval,
base::BindRepeating(
&ClientDiscardableSharedMemoryManager::ScheduledPurge,
base::Unretained(this)));
}
}
bool ClientDiscardableSharedMemoryManager::OnMemoryDump(
const base::trace_event::MemoryDumpArgs& args,
base::trace_event::ProcessMemoryDump* pmd) {
......@@ -336,10 +386,27 @@ size_t ClientDiscardableSharedMemoryManager::GetBytesAllocated() const {
return heap_->GetSize() - heap_->GetSizeOfFreeLists();
}
void ClientDiscardableSharedMemoryManager::PurgeUnlockedMemory() {
void ClientDiscardableSharedMemoryManager::BackgroundPurge() {
PurgeUnlockedMemory(base::TimeDelta());
}
void ClientDiscardableSharedMemoryManager::ScheduledPurge() {
// From local testing and UMA, memory usually accumulates slowly in renderers,
// and can sit idle for hours. We purge only the old memory, as this should
// recover the memory without adverse latency effects.
// TODO(crbug.com/1123679): Determine if |kMinAgeForScheduledPurge| and the
// constant from |ScheduledPurge| need to be tuned.
PurgeUnlockedMemory(
ClientDiscardableSharedMemoryManager::kMinAgeForScheduledPurge);
}
void ClientDiscardableSharedMemoryManager::PurgeUnlockedMemory(
base::TimeDelta min_age) {
{
base::AutoLock lock(lock_);
auto now = base::TimeTicks::Now();
// Iterate this way in order to avoid invalidating the iterator while
// removing elements from |allocated_memory_| as we iterate over it.
for (auto it = allocated_memory_.begin(); it != allocated_memory_.end();
......@@ -353,7 +420,7 @@ void ClientDiscardableSharedMemoryManager::PurgeUnlockedMemory() {
DCHECK_EQ(&lock_, &mem->manager_->GetLock());
mem->manager_->GetLock().AssertAcquired();
auto span = mem->Purge();
auto span = mem->Purge(now - min_age);
if (span) {
allocated_memory_.erase(prev);
ReleaseSpan(std::move(span));
......@@ -366,10 +433,13 @@ void ClientDiscardableSharedMemoryManager::PurgeUnlockedMemory() {
void ClientDiscardableSharedMemoryManager::ReleaseFreeMemory() {
if (base::FeatureList::IsEnabled(kPurgeUnlockedMemory)) {
PurgeUnlockedMemory();
BackgroundPurge();
} else {
ReleaseFreeMemoryImpl();
}
if (GetBytesAllocated() == 0)
timer_ = nullptr;
}
void ClientDiscardableSharedMemoryManager::ReleaseFreeMemoryImpl() {
......
......@@ -10,6 +10,7 @@
#include <memory>
#include <set>
#include "base/feature_list.h"
#include "base/memory/discardable_memory_allocator.h"
#include "base/memory/ref_counted.h"
#include "base/memory/unsafe_shared_memory_region.h"
......@@ -27,6 +28,8 @@ class SingleThreadTaskRunner;
namespace discardable_memory {
DISCARDABLE_MEMORY_EXPORT extern const base::Feature kSchedulePeriodicPurge;
// Implementation of DiscardableMemoryAllocator that allocates
// discardable memory segments through the browser process.
class DISCARDABLE_MEMORY_EXPORT ClientDiscardableSharedMemoryManager
......@@ -35,7 +38,9 @@ class DISCARDABLE_MEMORY_EXPORT ClientDiscardableSharedMemoryManager
public:
ClientDiscardableSharedMemoryManager(
mojo::PendingRemote<mojom::DiscardableSharedMemoryManager> manager,
scoped_refptr<base::SingleThreadTaskRunner> io_task_runner);
scoped_refptr<base::SingleThreadTaskRunner> io_task_runner,
scoped_refptr<base::SingleThreadTaskRunner> periodic_purge_task_runner =
nullptr);
~ClientDiscardableSharedMemoryManager() override;
// Overridden from base::DiscardableMemoryAllocator:
......@@ -46,8 +51,8 @@ class DISCARDABLE_MEMORY_EXPORT ClientDiscardableSharedMemoryManager
bool OnMemoryDump(const base::trace_event::MemoryDumpArgs& args,
base::trace_event::ProcessMemoryDump* pmd) override;
// Purge any unlocked memory that was allocated by this manager.
void PurgeUnlockedMemory();
// Purge all unlocked memory that was allocated by this manager.
void BackgroundPurge();
// Release memory and associated resources that have been purged.
void ReleaseFreeMemory() override;
......@@ -62,6 +67,9 @@ class DISCARDABLE_MEMORY_EXPORT ClientDiscardableSharedMemoryManager
const char* name,
base::trace_event::ProcessMemoryDump* pmd) const;
void StartScheduledPurging(
scoped_refptr<base::SequencedTaskRunner> task_runner);
struct Statistics {
size_t total_size;
size_t freelist_size;
......@@ -73,13 +81,19 @@ class DISCARDABLE_MEMORY_EXPORT ClientDiscardableSharedMemoryManager
bytes_allocated_limit_for_testing_ = limit;
}
// We only have protected members for testing, everything else should be
// either public or private.
static constexpr base::TimeDelta kMinAgeForScheduledPurge =
base::TimeDelta::FromMinutes(5);
// These fields are only protected for testing, they would otherwise be
// private. Everything else should be either public or private.
protected:
explicit ClientDiscardableSharedMemoryManager(
scoped_refptr<base::SingleThreadTaskRunner> io_task_runner);
ClientDiscardableSharedMemoryManager(
scoped_refptr<base::SingleThreadTaskRunner> io_task_runner,
scoped_refptr<base::SingleThreadTaskRunner> periodic_purge_task_runner);
std::unique_ptr<DiscardableSharedMemoryHeap> heap_ GUARDED_BY(lock_);
mutable base::Lock lock_;
std::unique_ptr<base::RepeatingTimer> timer_;
scoped_refptr<base::SingleThreadTaskRunner> periodic_purge_task_runner_;
private:
class DiscardableMemoryImpl : public base::DiscardableMemory {
......@@ -101,17 +115,27 @@ class DISCARDABLE_MEMORY_EXPORT ClientDiscardableSharedMemoryManager
const char* name,
base::trace_event::ProcessMemoryDump* pmd) const override;
// Returns |span_| if unlocked, otherwise nullptr.
std::unique_ptr<DiscardableSharedMemoryHeap::Span> Purge()
// Returns |span_| if it has been unlocked since at least |min_ticks|,
// otherwise nullptr.
std::unique_ptr<DiscardableSharedMemoryHeap::Span> Purge(
base::TimeTicks min_ticks)
EXCLUSIVE_LOCKS_REQUIRED(manager_->GetLock());
private:
bool is_locked() const EXCLUSIVE_LOCKS_REQUIRED(manager_->GetLock());
friend class ClientDiscardableSharedMemoryManager;
ClientDiscardableSharedMemoryManager* const manager_;
std::unique_ptr<DiscardableSharedMemoryHeap::Span> span_;
bool is_locked_ GUARDED_BY(manager_->GetLock());
// Set to an invalid base::TimeTicks when |this| is Lock()-ed, and to
// |TimeTicks::Now()| each time |this| is Unlock()-ed.
base::TimeTicks last_locked_ GUARDED_BY(manager_->GetLock());
};
// Purge any unlocked memory from foreground that hasn't been touched in a
// while.
void ScheduledPurge();
// This is only virtual for testing.
virtual std::unique_ptr<base::DiscardableSharedMemory>
AllocateLockedDiscardableSharedMemory(size_t size, int32_t id);
......@@ -128,6 +152,8 @@ class DISCARDABLE_MEMORY_EXPORT ClientDiscardableSharedMemoryManager
void MemoryUsageChanged(size_t new_bytes_allocated,
size_t new_bytes_free) const;
// Releases all unlocked memory that was last locked at least |min_age| ago.
void PurgeUnlockedMemory(base::TimeDelta min_age);
void ReleaseFreeMemoryImpl();
void ReleaseMemory(DiscardableMemoryImpl* memory,
std::unique_ptr<DiscardableSharedMemoryHeap::Span> span)
......
......@@ -8,6 +8,8 @@
#include "base/process/process_metrics.h"
#include "base/synchronization/lock.h"
#include "base/test/metrics/histogram_tester.h"
#include "base/test/scoped_feature_list.h"
#include "base/test/task_environment.h"
#include "testing/gtest/include/gtest/gtest.h"
namespace discardable_memory {
......@@ -40,9 +42,11 @@ class TestSingleThreadTaskRunner : public base::SingleThreadTaskRunner {
class TestClientDiscardableSharedMemoryManager
: public ClientDiscardableSharedMemoryManager {
public:
TestClientDiscardableSharedMemoryManager()
explicit TestClientDiscardableSharedMemoryManager(
scoped_refptr<base::SingleThreadTaskRunner> periodic_purge_task_runner)
: ClientDiscardableSharedMemoryManager(
base::MakeRefCounted<TestSingleThreadTaskRunner>()) {}
base::MakeRefCounted<TestSingleThreadTaskRunner>(),
periodic_purge_task_runner) {}
~TestClientDiscardableSharedMemoryManager() override = default;
......@@ -64,115 +68,131 @@ class TestClientDiscardableSharedMemoryManager
base::AutoLock lock(lock_);
return heap_->GetSizeOfFreeLists();
}
base::RepeatingTimer* timer() const { return timer_.get(); }
};
// This test allocates a single piece of memory, then verifies that calling
// |PurgeUnlockedMemory| only affects the memory when it is unlocked.
TEST(ClientDiscardableSharedMemoryManagerTest, Simple) {
const size_t page_size = base::GetPageSize();
TestClientDiscardableSharedMemoryManager client;
class ClientDiscardableSharedMemoryManagerTest : public testing::Test {
public:
void SetUp() override {
client_ =
std::make_unique<TestClientDiscardableSharedMemoryManager>(nullptr);
}
std::unique_ptr<TestClientDiscardableSharedMemoryManager> client_;
const size_t page_size_ = base::GetPageSize();
};
class ClientDiscardableSharedMemoryManagerPeriodicPurgingTest
: public ClientDiscardableSharedMemoryManagerTest {
public:
ClientDiscardableSharedMemoryManagerPeriodicPurgingTest()
: task_env_(base::test::TaskEnvironment::MainThreadType::UI,
base::test::TaskEnvironment::TimeSource::MOCK_TIME) {}
void SetUp() override {
client_ = std::make_unique<TestClientDiscardableSharedMemoryManager>(
task_env_.GetMainThreadTaskRunner());
}
base::test::TaskEnvironment task_env_;
};
// This test allocates a single piece of memory, then verifies that calling
// |BackgroundPurge| only affects the memory when it is unlocked.
TEST_F(ClientDiscardableSharedMemoryManagerTest, Simple) {
// Initially, we should have no memory allocated
ASSERT_EQ(client.GetBytesAllocated(), 0u);
ASSERT_EQ(client.GetSizeOfFreeLists(), 0u);
ASSERT_EQ(client_->GetBytesAllocated(), 0u);
ASSERT_EQ(client_->GetSizeOfFreeLists(), 0u);
auto mem = client.AllocateLockedDiscardableMemory(page_size);
auto mem = client_->AllocateLockedDiscardableMemory(page_size_);
// After allocation, we should have allocated a single piece of memory.
EXPECT_EQ(client.GetBytesAllocated(), page_size);
EXPECT_EQ(client_->GetBytesAllocated(), page_size_);
client.PurgeUnlockedMemory();
client_->BackgroundPurge();
// All our memory is locked, so calling |PurgeUnlockedMemory| should have no
// All our memory is locked, so calling |BackgroundPurge| should have no
// effect.
EXPECT_EQ(client.GetBytesAllocated(), base::GetPageSize());
EXPECT_EQ(client_->GetBytesAllocated(), base::GetPageSize());
mem->Unlock();
// Unlocking has no effect on the amount of memory we have allocated.
EXPECT_EQ(client.GetBytesAllocated(), base::GetPageSize());
EXPECT_EQ(client_->GetBytesAllocated(), base::GetPageSize());
client.PurgeUnlockedMemory();
client_->BackgroundPurge();
// Now that |mem| is unlocked, the call to |PurgeUnlockedMemory| will
// Now that |mem| is unlocked, the call to |BackgroundPurge| will
// remove it.
EXPECT_EQ(client.GetBytesAllocated(), 0u);
EXPECT_EQ(client_->GetBytesAllocated(), 0u);
}
// This test allocates multiple pieces of memory, then unlocks them one by one,
// verifying that |PurgeUnlockedMemory| only affects the unlocked pieces of
// verifying that |BackgroundPurge| only affects the unlocked pieces of
// memory.
TEST(ClientDiscardableSharedMemoryManagerTest, MultipleOneByOne) {
const size_t page_size = base::GetPageSize();
TestClientDiscardableSharedMemoryManager client;
TEST_F(ClientDiscardableSharedMemoryManagerTest, MultipleOneByOne) {
ASSERT_EQ(client_->GetBytesAllocated(), 0u);
ASSERT_EQ(client_->GetSizeOfFreeLists(), 0u);
ASSERT_EQ(client.GetBytesAllocated(), 0u);
ASSERT_EQ(client.GetSizeOfFreeLists(), 0u);
auto mem1 = client_->AllocateLockedDiscardableMemory(page_size_ * 2.2);
auto mem2 = client_->AllocateLockedDiscardableMemory(page_size_ * 1.1);
auto mem3 = client_->AllocateLockedDiscardableMemory(page_size_ * 3.5);
auto mem4 = client_->AllocateLockedDiscardableMemory(page_size_ * 0.2);
auto mem1 = client.AllocateLockedDiscardableMemory(page_size * 2.2);
auto mem2 = client.AllocateLockedDiscardableMemory(page_size * 1.1);
auto mem3 = client.AllocateLockedDiscardableMemory(page_size * 3.5);
auto mem4 = client.AllocateLockedDiscardableMemory(page_size * 0.2);
EXPECT_EQ(client.GetBytesAllocated(), 10 * page_size);
EXPECT_EQ(client_->GetBytesAllocated(), 10 * page_size_);
// Does nothing because everything is locked.
client.PurgeUnlockedMemory();
client_->BackgroundPurge();
EXPECT_EQ(client.GetBytesAllocated(), 10 * page_size);
EXPECT_EQ(client_->GetBytesAllocated(), 10 * page_size_);
mem1->Unlock();
// Does nothing, since we don't have any free memory, just unlocked memory.
client.ReleaseFreeMemory();
client_->ReleaseFreeMemory();
EXPECT_EQ(client.GetBytesAllocated(), 10 * page_size);
EXPECT_EQ(client_->GetBytesAllocated(), 10 * page_size_);
// This gets rid of |mem1| (which is unlocked), but not the rest of the
// memory.
client.PurgeUnlockedMemory();
client_->BackgroundPurge();
EXPECT_EQ(client.GetBytesAllocated(), 7 * page_size);
EXPECT_EQ(client_->GetBytesAllocated(), 7 * page_size_);
// We do similar checks to above for the rest of the memory.
mem2->Unlock();
client.PurgeUnlockedMemory();
client_->BackgroundPurge();
EXPECT_EQ(client.GetBytesAllocated(), 5 * page_size);
EXPECT_EQ(client_->GetBytesAllocated(), 5 * page_size_);
mem3->Unlock();
client.PurgeUnlockedMemory();
EXPECT_EQ(client.GetBytesAllocated(), 1 * page_size);
client_->BackgroundPurge();
EXPECT_EQ(client_->GetBytesAllocated(), 1 * page_size_);
mem4->Unlock();
client.PurgeUnlockedMemory();
EXPECT_EQ(client.GetBytesAllocated(), 0 * page_size);
client_->BackgroundPurge();
EXPECT_EQ(client_->GetBytesAllocated(), 0 * page_size_);
}
// This test allocates multiple pieces of memory, then unlocks them all,
// verifying that |PurgeUnlockedMemory| only affects the unlocked pieces of
// verifying that |BackgroundPurge| only affects the unlocked pieces of
// memory.
TEST(ClientDiscardableSharedMemoryManagerTest, MultipleAtOnce) {
const size_t page_size = base::GetPageSize();
TestClientDiscardableSharedMemoryManager client;
ASSERT_EQ(client.GetBytesAllocated(), 0u);
ASSERT_EQ(client.GetSizeOfFreeLists(), 0u);
TEST_F(ClientDiscardableSharedMemoryManagerTest, MultipleAtOnce) {
ASSERT_EQ(client_->GetBytesAllocated(), 0u);
ASSERT_EQ(client_->GetSizeOfFreeLists(), 0u);
auto mem1 = client.AllocateLockedDiscardableMemory(page_size * 2.2);
auto mem2 = client.AllocateLockedDiscardableMemory(page_size * 1.1);
auto mem3 = client.AllocateLockedDiscardableMemory(page_size * 3.5);
auto mem4 = client.AllocateLockedDiscardableMemory(page_size * 0.2);
auto mem1 = client_->AllocateLockedDiscardableMemory(page_size_ * 2.2);
auto mem2 = client_->AllocateLockedDiscardableMemory(page_size_ * 1.1);
auto mem3 = client_->AllocateLockedDiscardableMemory(page_size_ * 3.5);
auto mem4 = client_->AllocateLockedDiscardableMemory(page_size_ * 0.2);
EXPECT_EQ(client.GetBytesAllocated(), 10 * page_size);
EXPECT_EQ(client_->GetBytesAllocated(), 10 * page_size_);
// Does nothing because everything is locked.
client.PurgeUnlockedMemory();
client_->BackgroundPurge();
EXPECT_EQ(client.GetBytesAllocated(), 10 * page_size);
EXPECT_EQ(client_->GetBytesAllocated(), 10 * page_size_);
// Unlock all pieces of memory at once.
mem1->Unlock();
......@@ -180,100 +200,164 @@ TEST(ClientDiscardableSharedMemoryManagerTest, MultipleAtOnce) {
mem3->Unlock();
mem4->Unlock();
client.PurgeUnlockedMemory();
EXPECT_EQ(client.GetBytesAllocated(), 0 * page_size);
client_->BackgroundPurge();
EXPECT_EQ(client_->GetBytesAllocated(), 0 * page_size_);
}
// Tests that FreeLists are only released once all memory has been released.
TEST(ClientDiscardableSharedMemoryManagerTest, Release) {
const size_t page_size = base::GetPageSize();
TestClientDiscardableSharedMemoryManager client;
TEST_F(ClientDiscardableSharedMemoryManagerTest, Release) {
ASSERT_EQ(client_->GetBytesAllocated(), 0u);
ASSERT_EQ(client_->GetSizeOfFreeLists(), 0u);
ASSERT_EQ(client.GetBytesAllocated(), 0u);
ASSERT_EQ(client.GetSizeOfFreeLists(), 0u);
auto mem1 = client_->AllocateLockedDiscardableMemory(page_size_ * 3);
auto mem2 = client_->AllocateLockedDiscardableMemory(page_size_ * 2);
auto mem1 = client.AllocateLockedDiscardableMemory(page_size * 3);
auto mem2 = client.AllocateLockedDiscardableMemory(page_size * 2);
size_t freelist_size = client.GetSizeOfFreeLists();
EXPECT_EQ(client.GetBytesAllocated(), 5 * page_size);
size_t freelist_size = client_->GetSizeOfFreeLists();
EXPECT_EQ(client_->GetBytesAllocated(), 5 * page_size_);
mem1 = nullptr;
// Less memory is now allocated, but freelists are grown.
EXPECT_EQ(client.GetBytesAllocated(), page_size * 2);
EXPECT_EQ(client.GetSizeOfFreeLists(), freelist_size + page_size * 3);
EXPECT_EQ(client_->GetBytesAllocated(), page_size_ * 2);
EXPECT_EQ(client_->GetSizeOfFreeLists(), freelist_size + page_size_ * 3);
client.PurgeUnlockedMemory();
client_->BackgroundPurge();
// Purging doesn't remove any memory since none is unlocked, also doesn't
// remove freelists since we still have some.
EXPECT_EQ(client.GetBytesAllocated(), page_size * 2);
EXPECT_EQ(client.GetSizeOfFreeLists(), freelist_size + page_size * 3);
EXPECT_EQ(client_->GetBytesAllocated(), page_size_ * 2);
EXPECT_EQ(client_->GetSizeOfFreeLists(), freelist_size + page_size_ * 3);
mem2 = nullptr;
// No memory is allocated, but freelists are grown.
EXPECT_EQ(client.GetBytesAllocated(), 0u);
EXPECT_EQ(client.GetSizeOfFreeLists(), freelist_size + page_size * 5);
EXPECT_EQ(client_->GetBytesAllocated(), 0u);
EXPECT_EQ(client_->GetSizeOfFreeLists(), freelist_size + page_size_ * 5);
client.PurgeUnlockedMemory();
client_->BackgroundPurge();
// Purging now shrinks freelists as well.
EXPECT_EQ(client.GetBytesAllocated(), 0u);
EXPECT_EQ(client.GetSizeOfFreeLists(), 0u);
EXPECT_EQ(client_->GetBytesAllocated(), 0u);
EXPECT_EQ(client_->GetSizeOfFreeLists(), 0u);
}
// Similar to previous test, but makes sure that freelist still shrinks when
// last piece of memory was just unlocked instead of released.
TEST(ClientDiscardableSharedMemoryManagerTest, ReleaseUnlocked) {
const size_t page_size = base::GetPageSize();
TestClientDiscardableSharedMemoryManager client;
ASSERT_EQ(client.GetBytesAllocated(), 0u);
ASSERT_EQ(client.GetSizeOfFreeLists(), 0u);
TEST_F(ClientDiscardableSharedMemoryManagerTest, ReleaseUnlocked) {
ASSERT_EQ(client_->GetBytesAllocated(), 0u);
ASSERT_EQ(client_->GetSizeOfFreeLists(), 0u);
auto mem1 = client.AllocateLockedDiscardableMemory(page_size * 3);
auto mem2 = client.AllocateLockedDiscardableMemory(page_size * 2);
auto mem1 = client_->AllocateLockedDiscardableMemory(page_size_ * 3);
auto mem2 = client_->AllocateLockedDiscardableMemory(page_size_ * 2);
size_t freelist_size = client.GetSizeOfFreeLists();
EXPECT_EQ(client.GetBytesAllocated(), 5 * page_size);
size_t freelist_size = client_->GetSizeOfFreeLists();
EXPECT_EQ(client_->GetBytesAllocated(), 5 * page_size_);
mem1 = nullptr;
// Less memory is now allocated, but freelists are grown.
EXPECT_EQ(client.GetBytesAllocated(), page_size * 2);
EXPECT_EQ(client.GetSizeOfFreeLists(), freelist_size + page_size * 3);
EXPECT_EQ(client_->GetBytesAllocated(), page_size_ * 2);
EXPECT_EQ(client_->GetSizeOfFreeLists(), freelist_size + page_size_ * 3);
client.PurgeUnlockedMemory();
client_->BackgroundPurge();
// Purging doesn't remove any memory since none is unlocked, also doesn't
// remove freelists since we still have some.
EXPECT_EQ(client.GetBytesAllocated(), page_size * 2);
EXPECT_EQ(client.GetSizeOfFreeLists(), freelist_size + page_size * 3);
EXPECT_EQ(client_->GetBytesAllocated(), page_size_ * 2);
EXPECT_EQ(client_->GetSizeOfFreeLists(), freelist_size + page_size_ * 3);
mem2->Unlock();
// No change in memory usage, since memory was only unlocked not released.
EXPECT_EQ(client.GetBytesAllocated(), page_size * 2);
EXPECT_EQ(client.GetSizeOfFreeLists(), freelist_size + page_size * 3);
EXPECT_EQ(client_->GetBytesAllocated(), page_size_ * 2);
EXPECT_EQ(client_->GetSizeOfFreeLists(), freelist_size + page_size_ * 3);
client.PurgeUnlockedMemory();
client_->BackgroundPurge();
// Purging now shrinks freelists as well.
EXPECT_EQ(client.GetBytesAllocated(), 0u);
EXPECT_EQ(client.GetSizeOfFreeLists(), 0u);
EXPECT_EQ(client_->GetBytesAllocated(), 0u);
EXPECT_EQ(client_->GetSizeOfFreeLists(), 0u);
}
// This tests that memory is actually removed by the periodic purging. We mock a
// task runner for this test and fast forward to make sure that the memory is
// purged at the right time.
TEST_F(ClientDiscardableSharedMemoryManagerPeriodicPurgingTest,
ScheduledReleaseUnlocked) {
base::test::ScopedFeatureList fl;
fl.InitAndEnableFeature(discardable_memory::kSchedulePeriodicPurge);
ASSERT_EQ(client_->GetBytesAllocated(), 0u);
ASSERT_EQ(client_->GetSizeOfFreeLists(), 0u);
auto mem1 = client_->AllocateLockedDiscardableMemory(page_size_ * 3);
task_env_.FastForwardBy(
ClientDiscardableSharedMemoryManager::kMinAgeForScheduledPurge * 2);
EXPECT_EQ(client_->GetBytesAllocated(), 3 * page_size_);
mem1->Unlock();
task_env_.FastForwardBy(
ClientDiscardableSharedMemoryManager::kMinAgeForScheduledPurge * 2);
EXPECT_EQ(client_->GetBytesAllocated(), 0u);
}
// Same as the above test, but tests that multiple pieces of memory will be
// handled properly.
TEST_F(ClientDiscardableSharedMemoryManagerPeriodicPurgingTest,
ScheduledReleaseUnlockedMultiple) {
base::test::ScopedFeatureList fl;
fl.InitAndEnableFeature(discardable_memory::kSchedulePeriodicPurge);
ASSERT_EQ(client_->GetBytesAllocated(), 0u);
ASSERT_EQ(client_->GetSizeOfFreeLists(), 0u);
auto mem1 = client_->AllocateLockedDiscardableMemory(page_size_ * 3);
auto mem2 = client_->AllocateLockedDiscardableMemory(page_size_ * 2);
task_env_.FastForwardBy(
ClientDiscardableSharedMemoryManager::kMinAgeForScheduledPurge * 2);
EXPECT_EQ(client_->GetBytesAllocated(), 5 * page_size_);
mem1->Unlock();
EXPECT_EQ(client_->GetBytesAllocated(), 5 * page_size_);
// The purge only removes things that have been unlocked for at least
// |kMinAgeForScheduledPurge|
// minutes so this shouldn't remove anything.
task_env_.FastForwardBy(
ClientDiscardableSharedMemoryManager::kMinAgeForScheduledPurge / 2);
EXPECT_EQ(client_->GetBytesAllocated(), 5 * page_size_);
// The periodic purge should remove anything that's been locked for over
// |kMinAgeForScheduledPurge|
// minutes, so fast forward slightly more so that it happens.
task_env_.FastForwardBy(
ClientDiscardableSharedMemoryManager::kMinAgeForScheduledPurge / 2);
EXPECT_EQ(client_->GetBytesAllocated(), 2 * page_size_);
mem2->Unlock();
task_env_.FastForwardBy(
ClientDiscardableSharedMemoryManager::kMinAgeForScheduledPurge * 2);
EXPECT_EQ(client_->GetBytesAllocated(), 0u);
EXPECT_EQ(client_->GetSizeOfFreeLists(), 0u);
}
// Tests that the UMA for Lock()-ing sucesses
// Tests that the UMA for Lock()-ing successes
// ("Memory.Discardable.LockingSuccess") is recorded properly.
TEST(ClientDiscardableSharedMemoryManagerTest, LockingSuccessUma) {
TEST_F(ClientDiscardableSharedMemoryManagerTest, LockingSuccessUma) {
base::HistogramTester histograms;
TestClientDiscardableSharedMemoryManager client;
// number is arbitrary, we are only focused on whether the histogram is
// properly recorded in this test.
auto mem = client.AllocateLockedDiscardableMemory(200);
auto mem = client_->AllocateLockedDiscardableMemory(200);
histograms.ExpectBucketCount("Memory.Discardable.LockingSuccess", false, 0);
histograms.ExpectBucketCount("Memory.Discardable.LockingSuccess", true, 0);
......@@ -297,7 +381,7 @@ TEST(ClientDiscardableSharedMemoryManagerTest, LockingSuccessUma) {
// This should now fail because the unlocked memory was purged. This should
// add a sample to the failure bucket.
mem->Unlock();
client.PurgeUnlockedMemory();
client_->BackgroundPurge();
result = mem->Lock();
ASSERT_EQ(result, false);
......@@ -305,5 +389,70 @@ TEST(ClientDiscardableSharedMemoryManagerTest, LockingSuccessUma) {
histograms.ExpectBucketCount("Memory.Discardable.LockingSuccess", true, 2);
}
// Test that a repeating timer for background purging is created when we
// allocate memory and discarded when we run out of allocated memory.
TEST_F(ClientDiscardableSharedMemoryManagerPeriodicPurgingTest,
SchedulingProactivePurging) {
base::test::ScopedFeatureList fl;
fl.InitAndEnableFeature(discardable_memory::kSchedulePeriodicPurge);
ASSERT_EQ(client_->timer(), nullptr);
// the amount of memory allocated here is arbitrary, we're only trying to get
// the timer started.
auto mem = client_->AllocateLockedDiscardableMemory(200);
EXPECT_NE(client_->timer(), nullptr);
// This does not destroy the timer because there is still memory allocated.
client_->ReleaseFreeMemory();
EXPECT_NE(client_->timer(), nullptr);
mem = nullptr;
EXPECT_NE(client_->timer(), nullptr);
// Now that all memory is freed, destroy the timer.
client_->ReleaseFreeMemory();
EXPECT_EQ(client_->timer(), nullptr);
}
// This test is similar to the one above, but tests that creating and deleting
// the timer still works with multiple pieces of allocated memory.
TEST_F(ClientDiscardableSharedMemoryManagerPeriodicPurgingTest,
SchedulingProactivePurgingMultipleAllocations) {
base::test::ScopedFeatureList fl;
fl.InitAndEnableFeature(discardable_memory::kSchedulePeriodicPurge);
ASSERT_EQ(client_->timer(), nullptr);
// the amount of memory allocated here is arbitrary, we're only trying to get
// the timer started.
auto mem = client_->AllocateLockedDiscardableMemory(200);
auto mem2 = client_->AllocateLockedDiscardableMemory(100);
EXPECT_NE(client_->timer(), nullptr);
client_->ReleaseFreeMemory();
EXPECT_NE(client_->timer(), nullptr);
mem = nullptr;
EXPECT_NE(client_->timer(), nullptr);
client_->ReleaseFreeMemory();
EXPECT_NE(client_->timer(), nullptr);
mem2 = nullptr;
EXPECT_NE(client_->timer(), nullptr);
client_->ReleaseFreeMemory();
EXPECT_EQ(client_->timer(), nullptr);
}
} // namespace
} // namespace discardable_memory
......@@ -42,7 +42,8 @@ CreateDiscardableMemoryAllocator() {
manager_remote.InitWithNewPipeAndPassReceiver());
return std::make_unique<
discardable_memory::ClientDiscardableSharedMemoryManager>(
std::move(manager_remote), ChildProcess::current()->io_task_runner());
std::move(manager_remote), ChildProcess::current()->io_task_runner(),
ChildProcess::current()->main_thread()->main_thread_runner());
}
} // namespace content
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment