blob: 02f09310d313c448633519cfdee38d09e176843a [file] [edit]
// Copyright 2026 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include <lib/fit/defer.h>
#include <ktl/source_location.h>
#include <ktl/utility.h>
#include <vm/continuous_attribution_tracker.h>
#include "test_helper.h"
namespace vm_unittest {
namespace {
bool should_skip_no_feature(ktl::source_location caller = ktl::source_location::current()) {
if constexpr (EXPERIMENTAL_CONTINUOUS_PER_VMO_ATTRIBUTION_ENABLED) {
return false;
}
printf("Skipping %s; no support for continuous attribution feature detected.\n",
caller.function_name());
return true;
}
// Test that the continuous attribution tracker supports a "stubbed out" state.
bool continuous_attribution_tracker_stub() {
BEGIN_TEST;
StubContinuousAttributionTracker tracker;
tracker.Increment(1);
tracker.Decrement(1);
tracker.Increment(100);
tracker.Decrement(100);
tracker.Increment(100);
tracker.Increment(100);
tracker.Increment(100);
tracker.Decrement(150);
tracker.Decrement(150);
// Overflow is okay.
tracker.Decrement(3000);
// Do not call stub FetchCurrent and FetchHwmAndReset methods, as these unconditionally panic.
StubContinuousAttributionTracker assigned = ktl::move(tracker);
StubContinuousAttributionTracker moved(ktl::move(assigned));
ktl::ignore = moved;
END_TEST;
}
// Test that the initial state of the ContinuousAttributionTracker is zero.
bool continuous_attribution_tracker_create() {
BEGIN_TEST;
ContinuousAttributionTracker tracker;
EXPECT_EQ(0u, tracker.FetchCurrent());
EXPECT_EQ(0u, tracker.FetchHwmAndReset());
END_TEST;
}
// Test that the move and assignment transfers data to the new ContinuousAttributionTracker
// object.
bool continuous_attribution_tracker_transfer() {
BEGIN_TEST;
ContinuousAttributionTracker tracker;
tracker.Increment(5);
EXPECT_EQ(5u, tracker.FetchCurrent());
ContinuousAttributionTracker assigned_stats;
assigned_stats = ktl::move(tracker);
// The old one has nothing...
EXPECT_EQ(0u, tracker.FetchCurrent());
// but the new one has the data.
EXPECT_EQ(5u, assigned_stats.FetchCurrent());
ContinuousAttributionTracker constructed_stats(ktl::move(assigned_stats));
// The old one has nothing...
EXPECT_EQ(0u, assigned_stats.FetchCurrent());
// but the new one has the data.
EXPECT_EQ(5u, constructed_stats.FetchCurrent());
// Only inspect the high-water mark down here because if we checked before it would have been
// reset.
EXPECT_EQ(5u, constructed_stats.FetchHwmAndReset());
END_TEST;
}
// Test that the high-water mark accumulates values since last reset.
bool continuous_attribution_tracker_high_water_mark() {
BEGIN_TEST;
ContinuousAttributionTracker tracker;
tracker.Increment(5);
tracker.Decrement(5);
// The high-water mark is reset by the below.
EXPECT_EQ(5u, tracker.FetchHwmAndReset());
tracker.Increment(2);
tracker.Decrement(2);
tracker.Increment(3);
tracker.Decrement(2);
tracker.Decrement(1);
tracker.Increment(2);
EXPECT_EQ(2u, tracker.FetchCurrent());
// The high-water mark is 3 even though the current value is 2, since that was the highest since
// last reset.
EXPECT_EQ(3u, tracker.FetchHwmAndReset());
END_TEST;
}
// Test that the continuous attribution tracker supports large counts.
bool continuous_attribution_tracker_extreme() {
BEGIN_TEST;
ContinuousAttributionTracker tracker;
tracker.Increment(ktl::numeric_limits<uint32_t>::max());
EXPECT_EQ(ktl::numeric_limits<uint32_t>::max(), tracker.FetchCurrent());
END_TEST;
}
bool continuous_attribution_tracker_populate_vmo() {
BEGIN_TEST;
if (should_skip_no_feature()) {
END_TEST;
}
AutoVmScannerDisable disable_scanner;
fbl::RefPtr<VmObjectPaged> vmo;
zx_status_t status = VmObjectPaged::Create(PMM_ALLOC_FLAG_ANY, 0u, 3 * kPageSize, &vmo);
ASSERT_OK(status);
{
fbl::RefPtr<VmCowPages> cow_pages = vmo->DebugGetCowPages();
ASSERT_NONNULL(cow_pages);
EXPECT_EQ(0u, cow_pages->DebugGetPopulatedSlotsCount()); // There is no content.
}
// Write a non-zero value to the first two pages.
{
fbl::AllocChecker ac;
fbl::Vector<uint8_t> a;
a.resize(2 * kPageSize, 42, &ac);
ASSERT_TRUE(ac.check());
EXPECT_EQ(ZX_OK, vmo->Write(a.data(), 0, a.size()));
}
{
fbl::RefPtr<VmCowPages> cow_pages = vmo->DebugGetCowPages();
ASSERT_NONNULL(cow_pages);
EXPECT_EQ(2u, cow_pages->DebugGetPopulatedSlotsCount()); // There are two populated pages.
}
END_TEST;
}
// Test that the correct tracker is provided to the unidirectional clone and parent.
bool continuous_attribution_tracker_unidirectional_child() {
BEGIN_TEST;
if (should_skip_no_feature()) {
END_TEST;
}
AutoVmScannerDisable disable_scanner;
fbl::RefPtr<VmObjectPaged> vmo;
ASSERT_OK(make_partially_committed_pager_vmo(3, /*committed_pages=*/2, /*trap_dirty=*/false,
/*resizable=*/false, false, nullptr, &vmo));
// Create a unidirectional clone.
fbl::RefPtr<VmObject> child_vmo_no_paged;
ASSERT_OK(vmo->CreateClone(Resizability::NonResizable, SnapshotType::OnWrite, /*offset=*/0,
/*size=*/3 * kPageSize, /*copy_name=*/false, &child_vmo_no_paged));
// Assert there is no hidden parent (true unidirectional)
{
fbl::RefPtr<VmCowPages> hidden_parent = vmo->DebugGetCowPages()->DebugGetParent();
ASSERT_NULL(hidden_parent);
}
{
fbl::RefPtr<VmCowPages> cow_pages = vmo->DebugGetCowPages();
ASSERT_NONNULL(cow_pages);
// There are two pages committed in the parent.
EXPECT_EQ(2u, cow_pages->DebugGetPopulatedSlotsCount());
}
{
VmObjectPaged *child = DownCastVmObject<VmObjectPaged>(child_vmo_no_paged.get());
ASSERT_NONNULL(child);
fbl::RefPtr<VmCowPages> cow_pages = child->DebugGetCowPages();
ASSERT_NONNULL(cow_pages);
// There are no parent content markers in this hierarchy to track, as intended.
EXPECT_EQ(0u, cow_pages->DebugGetPopulatedSlotsCount());
}
END_TEST;
}
// Test that the correct tracker is provided to the bidirectional clone and parent.
bool continuous_attribution_tracker_bidirectional_child() {
BEGIN_TEST;
if (should_skip_no_feature()) {
END_TEST;
}
AutoVmScannerDisable disable_scanner;
fbl::RefPtr<VmObjectPaged> vmo;
ASSERT_OK(VmObjectPaged::Create(PMM_ALLOC_FLAG_ANY, 0, 3 * kPageSize, &vmo));
// Write a non-zero value to the first two pages.
{
fbl::AllocChecker ac;
fbl::Vector<uint8_t> a;
a.resize(2 * kPageSize, 42, &ac);
ASSERT_TRUE(ac.check());
EXPECT_EQ(ZX_OK, vmo->Write(a.data(), 0, a.size()));
}
// Create a bidirectional clone.
fbl::RefPtr<VmObject> child_vmo_no_paged;
ASSERT_OK(vmo->CreateClone(Resizability::NonResizable, SnapshotType::Full, /*offset=*/0,
/*size=*/3 * kPageSize, /*copy_name=*/false, &child_vmo_no_paged));
// Assert there is a hidden parent (true bidirectional)
fbl::RefPtr<VmCowPages> hidden_parent = vmo->DebugGetCowPages()->DebugGetParent();
ASSERT_NONNULL(hidden_parent);
{
fbl::RefPtr<VmCowPages> cow_pages = vmo->DebugGetCowPages();
ASSERT_NONNULL(cow_pages);
EXPECT_EQ(2u, cow_pages->DebugGetPopulatedSlotsCount());
}
{
VmObjectPaged *child = DownCastVmObject<VmObjectPaged>(child_vmo_no_paged.get());
ASSERT_NONNULL(child);
fbl::RefPtr<VmCowPages> cow_pages = child->DebugGetCowPages();
ASSERT_NONNULL(cow_pages);
EXPECT_EQ(2u, cow_pages->DebugGetPopulatedSlotsCount());
}
// There are two pages committed in the parent.
EXPECT_EQ(2u, hidden_parent->DebugGetPopulatedSlotsCount());
END_TEST;
}
// Test that zeroing an anonymous VMO decreases the populated slots count.
bool continuous_attribution_tracker_zero_anonymous() {
BEGIN_TEST;
if (should_skip_no_feature()) {
END_TEST;
}
AutoVmScannerDisable disable_scanner;
fbl::RefPtr<VmObjectPaged> vmo;
ASSERT_OK(VmObjectPaged::Create(PMM_ALLOC_FLAG_ANY, 0, 3 * kPageSize, &vmo));
fbl::RefPtr<VmCowPages> cow_pages = vmo->DebugGetCowPages();
ASSERT_NONNULL(cow_pages);
// Write a non-zero value to the first two pages.
{
fbl::AllocChecker ac;
fbl::Vector<uint8_t> a;
a.resize(2 * kPageSize, 42, &ac);
ASSERT_TRUE(ac.check());
EXPECT_EQ(ZX_OK, vmo->Write(a.data(), 0, a.size()));
}
EXPECT_EQ(2u, cow_pages->DebugGetPopulatedSlotsCount());
// Clear out one page, so that afterwards the VMO will only have one populated page.
{
__UNINITIALIZED MultiPageRequest page_request;
VmCowPages::DeferredOps deferred(cow_pages.get());
Guard<CriticalMutex> guard{cow_pages->lock()};
VmCowRange range(0, kPageSize);
// Directly call the lower-level interface, as opposed to the method on VmObject. The
// attribution for the higher-level method is incomplete.
auto [status, zeroed_bytes] =
cow_pages->ZeroPagesLocked(range, /*dirty_track=*/false, deferred, &page_request);
EXPECT_EQ(kPageSize, zeroed_bytes);
EXPECT_OK(status);
}
EXPECT_EQ(1u, cow_pages->DebugGetPopulatedSlotsCount());
END_TEST;
}
// Test that zeroing a pager-backed VMO decreases the populated slots count.
bool continuous_attribution_tracker_zero_pager_backed() {
BEGIN_TEST;
if (should_skip_no_feature()) {
END_TEST;
}
AutoVmScannerDisable disable_scanner;
fbl::RefPtr<VmObjectPaged> vmo;
ASSERT_OK(make_partially_committed_pager_vmo(3, /*committed_pages=*/2, /*trap_dirty=*/false,
/*resizable=*/false, false, nullptr, &vmo));
fbl::RefPtr<VmCowPages> cow_pages = vmo->DebugGetCowPages();
ASSERT_NONNULL(cow_pages);
EXPECT_EQ(2u, cow_pages->DebugGetPopulatedSlotsCount());
// Clear out one page, so that afterwards the VMO will only have one populated page.
{
__UNINITIALIZED MultiPageRequest page_request;
VmCowPages::DeferredOps deferred(cow_pages.get());
Guard<CriticalMutex> guard{cow_pages->lock()};
VmCowRange range(0, kPageSize);
// Directly call the lower-level interface, as opposed to the method on VmObject. The
// attribution for the higher-level method is incomplete.
auto [status, zeroed_bytes] =
cow_pages->ZeroPagesLocked(range, /*dirty_track=*/false, deferred, &page_request);
EXPECT_EQ(kPageSize, zeroed_bytes);
EXPECT_OK(status);
}
EXPECT_EQ(1u, cow_pages->DebugGetPopulatedSlotsCount());
END_TEST;
}
// Test that zeroing a child of a pager-backed VMO correctly updates the populated bytes count.
bool continuous_attribution_tracker_zero_pager_clone() {
BEGIN_TEST;
if (should_skip_no_feature()) {
END_TEST;
}
AutoVmScannerDisable disable_scanner;
fbl::RefPtr<VmObjectPaged> vmo;
ASSERT_OK(make_partially_committed_pager_vmo(3, /*committed_pages=*/2, /*trap_dirty=*/false,
/*resizable=*/false, false, nullptr, &vmo));
fbl::RefPtr<VmObject> child_vmo_no_paged;
ASSERT_OK(vmo->CreateClone(Resizability::NonResizable, SnapshotType::Modified, /*offset=*/0,
/*size=*/3 * kPageSize, /*copy_name=*/false, &child_vmo_no_paged));
fbl::RefPtr<VmObjectPaged> child = DownCastVmObject<VmObjectPaged>(child_vmo_no_paged);
ASSERT_NONNULL(child);
fbl::RefPtr<VmCowPages> child_cow_pages = child->DebugGetCowPages();
EXPECT_EQ(0u, child_cow_pages->DebugGetPopulatedSlotsCount());
ASSERT_OK(child->CommitRange(0, 2 * kPageSize));
EXPECT_EQ(2u, child_cow_pages->DebugGetPopulatedSlotsCount());
// Clear out one page, so that afterwards the VMO will only have one populated page.
{
__UNINITIALIZED MultiPageRequest page_request;
VmCowPages::DeferredOps deferred(child_cow_pages.get());
Guard<CriticalMutex> guard{child_cow_pages->lock()};
VmCowRange range(0, kPageSize);
// Directly call the lower-level interface, as opposed to the method on VmObject. The
// attribution for the higher-level method is incomplete.
auto [status, zeroed_bytes] =
child_cow_pages->ZeroPagesLocked(range, /*dirty_track=*/false, deferred, &page_request);
EXPECT_EQ(kPageSize, zeroed_bytes);
EXPECT_OK(status);
}
EXPECT_EQ(1u, child_cow_pages->DebugGetPopulatedSlotsCount());
END_TEST;
}
// Test that removing a page from a hidden parent decrements populated bytes count.
bool continuous_attribution_tracker_require_move_page() {
BEGIN_TEST;
if (should_skip_no_feature()) {
END_TEST;
}
AutoVmScannerDisable disable_scanner;
// Set up a hidden parent with two pages and children with no content.
fbl::RefPtr<VmObjectPaged> child1;
ASSERT_OK(VmObjectPaged::Create(PMM_ALLOC_FLAG_ANY, 0, 3 * kPageSize, &child1));
// Write a non-zero value to the first two pages.
fbl::AllocChecker ac;
fbl::Vector<uint8_t> a;
a.resize(2 * kPageSize, 42, &ac);
ASSERT_TRUE(ac.check());
EXPECT_EQ(ZX_OK, child1->Write(a.data(), 0, a.size()));
// Create a bidirectional clone.
fbl::RefPtr<VmObject> child2;
ASSERT_OK(child1->CreateClone(Resizability::NonResizable, SnapshotType::Full, /*offset=*/0,
/*size=*/3 * kPageSize, /*copy_name=*/false, &child2));
VmObjectPaged *child_paged = DownCastVmObject<VmObjectPaged>(child2.get());
ASSERT_NONNULL(child_paged);
fbl::RefPtr<VmCowPages> child2_cow = child_paged->DebugGetCowPages();
// Assert there is a hidden parent (true bidirectional)
fbl::RefPtr<VmCowPages> hidden_parent_cow = child2_cow->DebugGetParent();
ASSERT_NONNULL(hidden_parent_cow);
// Decrement the share count for the first page by making child1 copy-on-write the first page.
child1->CommitRange(0, kPageSize);
// Now the share count for the first page is just one in the hidden parent.
// The content is attributed to both the parent and the child because the pages are resident in
// the hidden parent and the child has parent content markers.
EXPECT_EQ(2u, hidden_parent_cow->DebugGetPopulatedSlotsCount());
EXPECT_EQ(2u, child2_cow->DebugGetPopulatedSlotsCount());
child2->CommitRange(0, kPageSize);
// The hidden parent's attribution count is decremented because it no longer has the page resident
// (it has been moved to the child).
EXPECT_EQ(1u, hidden_parent_cow->DebugGetPopulatedSlotsCount());
EXPECT_EQ(2u, child2_cow->DebugGetPopulatedSlotsCount());
END_TEST;
}
// Test that the populated slots count is decremented during the removal of parent content markers
// in hidden VMOs.
bool continuous_attribution_tracker_hidden_no_parent_content() {
BEGIN_TEST;
if (should_skip_no_feature()) {
END_TEST;
}
AutoVmScannerDisable disable_scanner;
// We will create a bidirectional clone chain with a 1) hidden root, 2) a hidden child of that
// root, and 3) a visible child of that child. When we create VMO #3, it will get a hidden parent
// whose parent content markers must be deleted from its page list. Ensure that it also decrements
// its populated slots count.
fbl::RefPtr<VmObjectPaged> vmo1;
ASSERT_OK(VmObjectPaged::Create(PMM_ALLOC_FLAG_ANY, 0, 3 * kPageSize, &vmo1));
ASSERT_OK(vmo1->CommitRange(0, 2 * kPageSize));
fbl::RefPtr<VmObject> vmo2_no_paged;
ASSERT_OK(vmo1->CreateClone(Resizability::NonResizable, SnapshotType::Full, /*offset=*/0,
/*size=*/3 * kPageSize, /*copy_name=*/false, &vmo2_no_paged));
fbl::RefPtr<VmObjectPaged> vmo2 = DownCastVmObject<VmObjectPaged>(vmo2_no_paged);
ASSERT_NONNULL(vmo2);
ASSERT_OK(vmo2->CommitRange(0, kPageSize));
fbl::RefPtr<VmObject> vmo3_no_paged;
ASSERT_OK(vmo2->CreateClone(Resizability::NonResizable, SnapshotType::Full, /*offset=*/0,
/*size=*/3 * kPageSize, /*copy_name=*/false, &vmo3_no_paged));
fbl::RefPtr<VmObjectPaged> vmo3 = DownCastVmObject<VmObjectPaged>(vmo3_no_paged);
ASSERT_NONNULL(vmo3);
fbl::RefPtr<VmCowPages> parent = vmo3->DebugGetCowPages()->DebugGetParent();
ASSERT_NONNULL(parent);
EXPECT_EQ(1u, parent->DebugGetPopulatedSlotsCount());
END_TEST;
}
// Test that the populated slots count is decremented when pages are evicted from VmCowPages.
bool continuous_attribution_tracker_reclaim_page() {
BEGIN_TEST;
if (should_skip_no_feature()) {
END_TEST;
}
AutoVmScannerDisable disable_scanner;
vm_page_t *committed_page;
fbl::RefPtr<VmObjectPaged> vmo;
ASSERT_OK(make_partially_committed_pager_vmo(3, /*committed_pages=*/1, /*trap_dirty=*/false,
/*resizable=*/false, false, &committed_page, &vmo));
EXPECT_EQ(1u, vmo->DebugGetCowPages()->DebugGetPopulatedSlotsCount());
ASSERT_OK(vmo->DebugGetCowPages()->EvictLoanedPage(committed_page, 0));
EXPECT_EQ(0u, vmo->DebugGetCowPages()->DebugGetPopulatedSlotsCount());
END_TEST;
}
// Test that the populated slots count is decremented when compression clears a slot after finding a
// zero page.
bool continuous_attribution_tracker_zero_page_compression() {
BEGIN_TEST;
if (should_skip_no_feature()) {
END_TEST;
}
AutoVmScannerDisable disable_scanner;
fbl::RefPtr<VmObjectPaged> vmo;
ASSERT_OK(VmObjectPaged::Create(PMM_ALLOC_FLAG_ANY, 0, kPageSize, &vmo));
ASSERT_OK(vmo->CommitRange(0, kPageSize));
EXPECT_EQ(1u, vmo->DebugGetCowPages()->DebugGetPopulatedSlotsCount());
vm_page_t *page = vmo->DebugGetPage(0);
ASSERT_NONNULL(page);
auto compression = Pmm::Node().GetPageCompression();
if (!compression) {
END_TEST;
}
auto compressor = compression->AcquireCompressor();
ASSERT_OK(compressor.get().Arm());
ASSERT_TRUE(vmo->DebugGetCowPages()
->ReclaimPage(page, 0, VmCowPages::EvictionAction::FollowHint, &compressor.get())
.is_ok());
EXPECT_EQ(0u, vmo->DebugGetCowPages()->DebugGetPopulatedSlotsCount());
END_TEST;
}
// Test that the populated slots count is decremented when zero page deduplication finds and removes
// a zero page.
bool continuous_attribution_tracker_zero_page_deduplication() {
BEGIN_TEST;
if (should_skip_no_feature()) {
END_TEST;
}
AutoVmScannerDisable disable_scanner;
fbl::RefPtr<VmObjectPaged> vmo;
ASSERT_OK(VmObjectPaged::Create(PMM_ALLOC_FLAG_ANY, 0, kPageSize, &vmo));
ASSERT_OK(vmo->CommitRange(0, kPageSize));
EXPECT_EQ(1u, vmo->DebugGetCowPages()->DebugGetPopulatedSlotsCount());
vm_page_t *page = vmo->DebugGetPage(0);
ASSERT_NONNULL(page);
ASSERT_TRUE(vmo->DebugGetCowPages()->DedupZeroPage(page, 0));
EXPECT_EQ(0u, vmo->DebugGetCowPages()->DebugGetPopulatedSlotsCount());
END_TEST;
}
// Test that content removed from a hidden parent due to 0 share count is reflected in the populated
// slots count.
bool continuous_attribution_tracker_release_hidden() {
BEGIN_TEST;
if (should_skip_no_feature()) {
END_TEST;
}
AutoVmScannerDisable disable_scanner;
fbl::RefPtr<VmObjectPaged> vmo;
ASSERT_OK(VmObjectPaged::Create(PMM_ALLOC_FLAG_ANY, 0, 2 * kPageSize, &vmo));
ASSERT_OK(vmo->CommitRange(0, 2 * kPageSize));
fbl::RefPtr<VmObject> child;
ASSERT_OK(vmo->CreateClone(Resizability::NonResizable, SnapshotType::Full, /*offset=*/0,
/*size=*/2 * kPageSize, /*copy_name=*/false, &child));
// Commit the pages in the child to remove its share count of the content.
ASSERT_OK(child->CommitRange(0, 2 * kPageSize));
fbl::RefPtr<VmCowPages> parent_cow = vmo->DebugGetCowPages()->DebugGetParent();
EXPECT_EQ(2u, parent_cow->DebugGetPopulatedSlotsCount());
// Remove the remaining share count for the first page, which will trigger the hidden parent to
// remove the content from its local page list.
{
fbl::RefPtr<VmCowPages> vmo_cow = vmo->DebugGetCowPages();
__UNINITIALIZED MultiPageRequest page_request;
VmCowPages::DeferredOps deferred(vmo_cow.get());
Guard<CriticalMutex> guard{vmo_cow->lock()};
VmCowRange range(0, kPageSize);
// ZeroPagesLocked calls DecrementCowContentShareCount, which is what we're interested in
// tracking.
auto [status, zeroed_bytes] =
vmo_cow->ZeroPagesLocked(range, /*dirty_track=*/false, deferred, &page_request);
EXPECT_EQ(kPageSize, zeroed_bytes);
EXPECT_OK(status);
}
EXPECT_EQ(1u, parent_cow->DebugGetPopulatedSlotsCount());
END_TEST;
}
// Test that DecommitRange decrements the populated slots count based on the number of populated
// pages it removes.
bool continuous_attribution_tracker_decommit_range() {
BEGIN_TEST;
if (should_skip_no_feature()) {
END_TEST;
}
AutoVmScannerDisable disable_scanner;
fbl::RefPtr<VmObjectPaged> vmo;
ASSERT_OK(VmObjectPaged::Create(PMM_ALLOC_FLAG_ANY, 0, 2 * kPageSize, &vmo));
EXPECT_EQ(0u, vmo->DebugGetCowPages()->DebugGetPopulatedSlotsCount());
ASSERT_OK(vmo->CommitRange(0, 2 * kPageSize));
EXPECT_EQ(2u, vmo->DebugGetCowPages()->DebugGetPopulatedSlotsCount());
ASSERT_OK(vmo->DecommitRange(0, kPageSize));
EXPECT_EQ(1u, vmo->DebugGetCowPages()->DebugGetPopulatedSlotsCount());
END_TEST;
}
// Check that DetachSource decrements the populated slots count for the removal of clean content.
bool continuous_attribution_tracker_detach_source() {
BEGIN_TEST;
if (should_skip_no_feature()) {
END_TEST;
}
AutoVmScannerDisable disable_scanner;
fbl::RefPtr<VmObjectPaged> vmo;
ASSERT_OK(make_partially_committed_pager_vmo(3, /*committed_pages=*/2, /*trap_dirty=*/false,
/*resizable=*/false, false, nullptr, &vmo));
EXPECT_EQ(2u, vmo->DebugGetCowPages()->DebugGetPopulatedSlotsCount());
vmo->DetachSource();
EXPECT_EQ(0u, vmo->DebugGetCowPages()->DebugGetPopulatedSlotsCount());
END_TEST;
}
// Test that the populated slots count is decremented when loaned pages are removed from a VMO as
// it's upgraded to being high priority.
bool continuous_attribution_tracker_remove_loaned_high_priority() {
BEGIN_TEST;
if (should_skip_no_feature()) {
END_TEST;
}
AutoVmScannerDisable disable_scanner;
const bool loaning_was_enabled = PhysicalPageBorrowingConfig::Get().is_loaning_enabled();
PhysicalPageBorrowingConfig::Get().set_loaning_enabled(true);
auto cleanup = fit::defer([loaning_was_enabled] {
PhysicalPageBorrowingConfig::Get().set_loaning_enabled(loaning_was_enabled);
});
// Provide a place for ReplacePageWithLoaned to borrow from.
fbl::RefPtr<VmObjectPaged> contiguous_vmo;
ASSERT_OK(VmObjectPaged::CreateContiguous(PMM_ALLOC_FLAG_ANY, kPageSize, /*alignment_log2=*/0,
&contiguous_vmo));
ASSERT_OK(contiguous_vmo->DecommitRange(0, kPageSize));
fbl::RefPtr<VmObjectPaged> vmo;
vm_page_t *before_page;
ASSERT_OK(make_committed_pager_vmo(1, /*trap_dirty=*/false,
/*resizable=*/false, &before_page, &vmo));
fbl::RefPtr<VmCowPages> cow_pages = vmo->DebugGetCowPages();
ASSERT_NONNULL(before_page);
ASSERT_OK(cow_pages->ReplacePageWithLoaned(before_page, /*offset=*/0));
auto change_priority = [&vmo](int64_t delta) {
PriorityChanger pc = vmo->MakePriorityChanger(delta);
if (delta > 0) {
pc.PrepareMayNotAlreadyBeHighPriority();
}
Guard<CriticalMutex> guard{AliasedLock, vmo->lock(), pc.lock()};
pc.ChangeHighPriorityCountLocked();
};
EXPECT_EQ(1u, vmo->DebugGetCowPages()->DebugGetPopulatedSlotsCount());
change_priority(1);
EXPECT_EQ(0u, vmo->DebugGetCowPages()->DebugGetPopulatedSlotsCount());
change_priority(-1);
EXPECT_EQ(0u, vmo->DebugGetCowPages()->DebugGetPopulatedSlotsCount());
END_TEST;
}
// Test that failing to add a sequence of pages correctly updates the populated slots count on
// cleanup.
bool continuous_attribution_tracker_add_pages() {
BEGIN_TEST;
if (should_skip_no_feature()) {
END_TEST;
}
AutoVmScannerDisable disable_scanner;
fbl::RefPtr<VmObjectPaged> vmo;
ASSERT_OK(VmObjectPaged::Create(PMM_ALLOC_FLAG_ANY, 0, 4 * kPageSize, &vmo));
fbl::RefPtr<VmCowPages> vmo_cow = vmo->DebugGetCowPages();
EXPECT_EQ(0u, vmo_cow->DebugGetPopulatedSlotsCount());
ASSERT_OK(vmo->CommitRange(kPageSize, kPageSize));
EXPECT_EQ(1u, vmo_cow->DebugGetPopulatedSlotsCount());
{
VmCowPages::DeferredOps deferred(vmo_cow.get());
Guard<CriticalMutex> guard{vmo_cow->lock()};
list_node list = LIST_INITIAL_VALUE(list);
const size_t count = 3;
ASSERT_OK(pmm_alloc_pages(count, 0, &list));
auto cleanup = fit::defer([&]() { pmm_free(&list); });
EXPECT_EQ(ZX_ERR_ALREADY_EXISTS,
vmo_cow->AddNewPagesLocked(0, &list, VmCowPages::CanOverwriteSlot::Empty,
/*zero=*/true, &deferred));
}
EXPECT_EQ(1u, vmo_cow->DebugGetPopulatedSlotsCount());
END_TEST;
}
// Regression test for https://fxbug.dev/483815044. Transfer a spurious parent content marker to a
// child, and check that it can be decommitted successfully.
bool continuous_attribution_tracker_merge_spurious_parent_content() {
BEGIN_TEST;
if (should_skip_no_feature()) {
END_TEST;
}
AutoVmScannerDisable disable_scanner;
fbl::RefPtr<VmObjectPaged> child;
// Make |child|'s only slot hold a parent content marker.
{
fbl::RefPtr<VmObjectPaged> vmo;
ASSERT_OK(VmObjectPaged::Create(PMM_ALLOC_FLAG_ANY, 0, 2 * kPageSize, &vmo));
// Ensure we get a bidirectional clone.
ASSERT_OK(vmo->CommitRange(0, 2 * kPageSize));
fbl::RefPtr<VmObject> child_no_paged;
ASSERT_OK(vmo->CreateClone(Resizability::NonResizable, SnapshotType::Full, 0, kPageSize,
/*copy_name=*/false, &child_no_paged));
child = DownCastVmObject<VmObjectPaged>(child_no_paged);
fbl::RefPtr<VmCowPages> hidden_parent = vmo->DebugGetCowPages()->DebugGetParent();
ASSERT_NONNULL(hidden_parent);
vm_page_t *page = hidden_parent->DebugGetPage(0);
VmCompression *compression = Pmm::Node().GetPageCompression();
if (!compression) {
END_TEST;
}
auto compressor = compression->AcquireCompressor();
ASSERT_OK(compressor.get().Arm());
auto result = hidden_parent->ReclaimPage(page, 0, VmCowPages::EvictionAction::IgnoreHint,
&compressor.get());
ASSERT_TRUE(result.is_ok());
EXPECT_EQ(result.value().num_pages, 1u);
// The content was removed (we actually compressed the zero page).
EXPECT_TRUE(hidden_parent->DebugIsEmpty(0));
// There is now a spurious parent content marker.
EXPECT_TRUE(child->DebugGetCowPages()->DebugIsParentContent(0));
// TODO(https://fxbug.dev/504652289): The continuous attribution system and the populated slots
// count should eventually agree here that there are zero populated slots and zero populated
// bytes.
EXPECT_EQ(1u, child->DebugGetCowPages()->DebugGetPopulatedSlotsCount());
EXPECT_EQ(0u, child->GetAttributedMemory().total_bytes());
// Let's drop |vmo| to trigger the hidden parent to merge into |child|. That will allow
// |child| to have no parent while still having a spurious parent content marker.
}
// There is 1 parent content marker.
EXPECT_TRUE(child->DebugGetCowPages()->DebugIsParentContent(0));
EXPECT_EQ(1u, child->DebugGetCowPages()->DebugGetPopulatedSlotsCount());
EXPECT_OK(child->DecommitRange(0, kPageSize));
EXPECT_TRUE(child->DebugGetCowPages()->DebugIsEmpty(0));
EXPECT_EQ(0u, child->DebugGetCowPages()->DebugGetPopulatedSlotsCount());
END_TEST;
}
// Test that the dead transition of a VMO correctly redistributes content between hidden parents and
// children.
bool continuous_attribution_tracker_merge_into_child() {
BEGIN_TEST;
if (should_skip_no_feature()) {
END_TEST;
}
AutoVmScannerDisable disable_scanner;
// Construct a copy-on-write hierarchy, and selectively destroy one leaf.
fbl::RefPtr<VmCowPages> a_cow;
fbl::RefPtr<VmObjectPaged> b;
fbl::RefPtr<VmObjectPaged> c;
fbl::RefPtr<VmCowPages> b_c_hidden_parent;
fbl::RefPtr<VmCowPages> a_hidden_parent;
{
fbl::RefPtr<VmObjectPaged> a;
ASSERT_OK(VmObjectPaged::Create(PMM_ALLOC_FLAG_ANY, 0, 3 * kPageSize, &a));
ASSERT_OK(a->CommitRange(0, kPageSize));
{
fbl::RefPtr<VmObject> b_no_paged;
ASSERT_OK(a->CreateClone(Resizability::NonResizable, SnapshotType::Full, /*offset=*/0,
/*size=*/3 * kPageSize, /*copy_name=*/false, &b_no_paged));
ASSERT_NONNULL(b_no_paged);
b = DownCastVmObject<VmObjectPaged>(b_no_paged);
ASSERT_NONNULL(b);
}
// Ensure that we get an extra level in the hierarchy.
ASSERT_OK(b->CommitRange(kPageSize, kPageSize));
{
fbl::RefPtr<VmObject> c_no_paged;
ASSERT_OK(b->CreateClone(Resizability::NonResizable, SnapshotType::Full, /*offset=*/0,
/*size=*/3 * kPageSize, /*copy_name=*/false, &c_no_paged));
ASSERT_NONNULL(c_no_paged);
c = DownCastVmObject<VmObjectPaged>(c_no_paged);
ASSERT_NONNULL(c);
}
// b and c have the same parent
fbl::RefPtr<VmCowPages> b_cow = b->DebugGetCowPages();
fbl::RefPtr<VmCowPages> c_cow = c->DebugGetCowPages();
EXPECT_EQ(b_cow->DebugGetParent().get(), c_cow->DebugGetParent().get());
b_c_hidden_parent = b_cow->DebugGetParent();
ASSERT_NONNULL(b_c_hidden_parent);
// b and c's parent's parent is the same as a's parent
a_cow = a->DebugGetCowPages();
EXPECT_EQ(b_c_hidden_parent->DebugGetParent().get(), a_cow->DebugGetParent().get());
a_hidden_parent = a_cow->DebugGetParent();
// Watch |b_c_hidden_parent|'s and |a_hidden_parent| populated slots as |a| dies.
EXPECT_EQ(1u, b_c_hidden_parent->DebugGetPopulatedSlotsCount());
EXPECT_EQ(1u, a_hidden_parent->DebugGetPopulatedSlotsCount());
}
EXPECT_EQ(2u, b_c_hidden_parent->DebugGetPopulatedSlotsCount());
EXPECT_EQ(0u, a_hidden_parent->DebugGetPopulatedSlotsCount());
END_TEST;
}
// Test that ReleaseOwnedPagesRangeLocked correctly updates the populated slot count when it can
// work entirely within the local page list.
bool continuous_attribution_tracker_release_owned_self() {
BEGIN_TEST;
if (should_skip_no_feature()) {
END_TEST;
}
AutoVmScannerDisable disable_scanner;
fbl::RefPtr<VmObjectPaged> vmo;
ASSERT_OK(
VmObjectPaged::Create(PMM_ALLOC_FLAG_ANY, VmObjectPaged::kResizable, 10 * kPageSize, &vmo));
fbl::RefPtr<VmCowPages> vmo_cow = vmo->DebugGetCowPages();
ASSERT_OK(vmo->CommitRange(0, 10 * kPageSize));
EXPECT_EQ(10u, vmo_cow->DebugGetPopulatedSlotsCount());
// Call ReleaseOwnedPagesRangeLocked indirectly through Resize.
ASSERT_OK(vmo->Resize(4 * kPageSize));
EXPECT_EQ(4u, vmo_cow->DebugGetPopulatedSlotsCount());
END_TEST;
}
// Test that ReleaseOwnedPagesRangeLocked correctly updates the populated slot count in hidden
// parents.
bool continuous_attribution_tracker_release_owned_parent() {
BEGIN_TEST;
if (should_skip_no_feature()) {
END_TEST;
}
AutoVmScannerDisable disable_scanner;
fbl::RefPtr<VmObjectPaged> a;
ASSERT_OK(
VmObjectPaged::Create(PMM_ALLOC_FLAG_ANY, VmObjectPaged::kResizable, 2 * kPageSize, &a));
ASSERT_OK(a->CommitRange(0, 2 * kPageSize));
fbl::RefPtr<VmObject> b_no_paged;
ASSERT_OK(a->CreateClone(Resizability::NonResizable, SnapshotType::Full, /*offset=*/0,
/*size=*/2 * kPageSize, /*copy_name=*/false, &b_no_paged));
ASSERT_NONNULL(b_no_paged);
fbl::RefPtr<VmObjectPaged> b = DownCastVmObject<VmObjectPaged>(b_no_paged);
ASSERT_NONNULL(b);
// Decrement the share count of the parent content.
ASSERT_OK(b->CommitRange(kPageSize, kPageSize));
fbl::RefPtr<VmCowPages> a_cow = a->DebugGetCowPages();
fbl::RefPtr<VmCowPages> parent_cow = a_cow->DebugGetParent();
ASSERT_NONNULL(parent_cow);
EXPECT_EQ(2u, a_cow->DebugGetPopulatedSlotsCount());
EXPECT_EQ(2u, parent_cow->DebugGetPopulatedSlotsCount());
// Call ReleaseOwnedPagesRangeLocked indirectly through Resize.
ASSERT_OK(a->Resize(kPageSize));
EXPECT_EQ(1u, a_cow->DebugGetPopulatedSlotsCount());
EXPECT_EQ(1u, parent_cow->DebugGetPopulatedSlotsCount());
END_TEST;
}
// Test that TakePages decrements the populated slots count in response to content it removes from
// the page list.
bool continuous_attribution_tracker_take_pages() {
BEGIN_TEST;
if (should_skip_no_feature()) {
END_TEST;
}
AutoVmScannerDisable disable_scanner;
fbl::RefPtr<VmObjectPaged> vmo;
ASSERT_OK(VmObjectPaged::Create(PMM_ALLOC_FLAG_ANY, 0, 3 * kPageSize, &vmo));
fbl::RefPtr<VmCowPages> vmo_cow = vmo->DebugGetCowPages();
ASSERT_OK(vmo->CommitRange(0, 3 * kPageSize));
EXPECT_EQ(3u, vmo_cow->DebugGetPopulatedSlotsCount());
VmPageSpliceList page_list;
ASSERT_OK(vmo->TakePages(0, 3 * kPageSize, &page_list));
EXPECT_EQ(0u, vmo_cow->DebugGetPopulatedSlotsCount());
END_TEST;
}
// Test that TakePages decrements the populated slots count in response to content it removes from
// the page list if there is a parent.
bool continuous_attribution_tracker_take_pages_parent() {
BEGIN_TEST;
if (should_skip_no_feature()) {
END_TEST;
}
// This is a separate test from continuous_attribution_tracker_take_pages because it triggers an
// independent branch in VmCowPages::TakePages.
AutoVmScannerDisable disable_scanner;
fbl::RefPtr<VmObjectPaged> a;
ASSERT_OK(VmObjectPaged::Create(PMM_ALLOC_FLAG_ANY, 0, 3 * kPageSize, &a));
ASSERT_OK(a->CommitRange(0, 3 * kPageSize));
fbl::RefPtr<VmObject> b;
ASSERT_OK(a->CreateClone(Resizability::NonResizable, SnapshotType::Full, /*offset=*/0,
/*size=*/3 * kPageSize, /*copy_name=*/false, &b));
ASSERT_OK(a->CommitRange(0, 3 * kPageSize));
fbl::RefPtr<VmCowPages> a_cow = a->DebugGetCowPages();
EXPECT_EQ(3u, a_cow->DebugGetPopulatedSlotsCount());
VmPageSpliceList page_list;
ASSERT_OK(a->TakePages(0, 3 * kPageSize, &page_list));
EXPECT_EQ(0u, a_cow->DebugGetPopulatedSlotsCount());
END_TEST;
}
// Test that deduplicating a zero page in a hidden parent creates a persistent
// disconnect between the child's attribution tracker and GetAttributedMemory.
//
// TODO(https://fxbug.dev/504652289): Update this test when the disconnect has been repaired.
bool continuous_attribution_tracker_dedup_hidden_parent_disconnect() {
BEGIN_TEST;
if (should_skip_no_feature()) {
END_TEST;
}
AutoVmScannerDisable disable_scanner;
fbl::RefPtr<VmObjectPaged> vmo;
ASSERT_OK(VmObjectPaged::Create(PMM_ALLOC_FLAG_ANY, 0, kPageSize, &vmo));
// Ensure there is a resident (zero) page.
ASSERT_OK(vmo->CommitRange(0, kPageSize));
fbl::RefPtr<VmObject> clone_no_paged;
ASSERT_OK(vmo->CreateClone(Resizability::NonResizable, SnapshotType::Full, 0, kPageSize, false,
&clone_no_paged));
fbl::RefPtr<VmObjectPaged> clone = DownCastVmObject<VmObjectPaged>(clone_no_paged);
fbl::RefPtr<VmCowPages> hidden_parent = vmo->DebugGetCowPages()->DebugGetParent();
ASSERT_NONNULL(hidden_parent);
EXPECT_EQ(1u, hidden_parent->DebugGetPopulatedSlotsCount());
EXPECT_EQ(kPageSize, vmo->GetAttributedMemory().total_bytes());
vm_page_t *page = hidden_parent->DebugGetPage(0);
ASSERT_NONNULL(page);
ASSERT_TRUE(hidden_parent->DedupZeroPage(page, 0));
// Note the disconnect between the continuously tracked populated slots count and
// GetAttributedMemory.
EXPECT_EQ(1u, vmo->DebugGetCowPages()->DebugGetPopulatedSlotsCount());
EXPECT_EQ(0u, vmo->GetAttributedMemory().total_bytes());
END_TEST;
}
UNITTEST_START_TESTCASE(continuous_attribution_tests)
VM_UNITTEST(continuous_attribution_tracker_stub)
VM_UNITTEST(continuous_attribution_tracker_create)
VM_UNITTEST(continuous_attribution_tracker_transfer)
VM_UNITTEST(continuous_attribution_tracker_high_water_mark)
VM_UNITTEST(continuous_attribution_tracker_extreme)
VM_UNITTEST(continuous_attribution_tracker_populate_vmo)
VM_UNITTEST(continuous_attribution_tracker_unidirectional_child)
VM_UNITTEST(continuous_attribution_tracker_bidirectional_child)
VM_UNITTEST(continuous_attribution_tracker_zero_anonymous)
VM_UNITTEST(continuous_attribution_tracker_zero_pager_backed)
VM_UNITTEST(continuous_attribution_tracker_zero_pager_clone)
VM_UNITTEST(continuous_attribution_tracker_require_move_page)
VM_UNITTEST(continuous_attribution_tracker_hidden_no_parent_content)
VM_UNITTEST(continuous_attribution_tracker_reclaim_page)
VM_UNITTEST(continuous_attribution_tracker_zero_page_compression)
VM_UNITTEST(continuous_attribution_tracker_zero_page_deduplication)
VM_UNITTEST(continuous_attribution_tracker_release_hidden)
VM_UNITTEST(continuous_attribution_tracker_decommit_range)
VM_UNITTEST(continuous_attribution_tracker_detach_source)
VM_UNITTEST(continuous_attribution_tracker_remove_loaned_high_priority)
VM_UNITTEST(continuous_attribution_tracker_add_pages)
VM_UNITTEST(continuous_attribution_tracker_merge_spurious_parent_content)
VM_UNITTEST(continuous_attribution_tracker_merge_into_child)
VM_UNITTEST(continuous_attribution_tracker_release_owned_self)
VM_UNITTEST(continuous_attribution_tracker_release_owned_parent)
VM_UNITTEST(continuous_attribution_tracker_take_pages)
VM_UNITTEST(continuous_attribution_tracker_take_pages_parent)
VM_UNITTEST(continuous_attribution_tracker_dedup_hidden_parent_disconnect)
UNITTEST_END_TESTCASE(continuous_attribution_tests, "continuous_attribution",
"Tests for populated bytes high-water mark")
} // namespace
} // namespace vm_unittest