blob: 0b8505dd9c4dc49437d24befd2a4bdb4d82d6cdb [file] [log] [blame]
// Copyright 2021 The Chromium OS 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 "vm_tools/concierge/balloon_policy.h"
#include "vm_tools/concierge/vm_util.h"
#include <string>
#include <base/files/file_path.h>
#include <base/files/file_util.h>
#include <base/json/json_reader.h>
#include <gmock/gmock.h>
#include <gtest/gtest.h>
namespace vm_tools {
namespace concierge {
// Test that shared and unevictable memory is subtracted from disk caches when
// checking if the guest has low caches.
TEST(BalloonPolocyTest, Unreclaimable) {
// Values are roughly what a 4GB ARCVM would get (but rounded)
const int64_t host_lwm = 200 * MIB;
ZoneInfoStats guest_stats = {.sum_low = 200 * MIB, .totalreserve = 300 * MIB};
MemoryMargins margins = {.critical = 400 * MIB, .moderate = 2000 * MIB};
const LimitCacheBalloonPolicy::Params params = {
.reclaim_target_cache = 0,
.critical_target_cache = 0,
.moderate_target_cache = 200 * MIB};
LimitCacheBalloonPolicy policy(margins, host_lwm, guest_stats, params,
"test");
// Test that when, because of unevictable memory, there is less cache left
// than the cache limit, that we keep free_memory at MaxFree.
{
BalloonStats stats = {.disk_caches = 300 * MIB,
.free_memory = policy.MaxFree(),
.unevictable_memory = 101 * MIB};
EXPECT_EQ(0, policy.ComputeBalloonDeltaImpl(
0 /* host_free */, stats,
margins.moderate /* host_available */, false, "test"));
}
// Test that when, because of shared memory, there is less cache left than the
// cache limit, that we keep free_memory at MaxFree.
{
BalloonStats stats = {.disk_caches = 300 * MIB,
.free_memory = policy.MaxFree(),
.shared_memory = 101 * MIB};
EXPECT_EQ(0, policy.ComputeBalloonDeltaImpl(
0 /* host_free */, stats,
margins.moderate /* host_available */, false, "test"));
}
}
// Test that having no limits still inflates the balloon to reduce excess free.
TEST(BalloonPolicyTest, LimitCacheNoLimit) {
const int64_t host_lwm = 200 * MIB;
ZoneInfoStats guest_stats = {.sum_low = 200 * MIB, .totalreserve = 300 * MIB};
const MemoryMargins margins = {.critical = 400 * MIB, .moderate = 2000 * MIB};
const LimitCacheBalloonPolicy::Params params = {.reclaim_target_cache = 0,
.critical_target_cache = 0,
.moderate_target_cache = 0};
LimitCacheBalloonPolicy policy(margins, host_lwm, guest_stats, params,
"test");
// NB: Because there are no cache limits, target_free will always be
// MaxFree().
// Test that we don't inflate the balloon if it's just a little bit.
{
const BalloonStats stats = {.disk_caches = 0,
.free_memory = policy.MaxFree() + MIB};
EXPECT_EQ(0, policy.ComputeBalloonDeltaImpl(0 /* host_free */, stats,
0 /* host_available */, false,
"test"));
}
// Test that we do inflate the balloon if it's a lot (twice MaxFree()).
{
const BalloonStats stats = {.disk_caches = 0,
.free_memory = policy.MaxFree() * 2};
EXPECT_EQ(policy.MaxFree(), policy.ComputeBalloonDeltaImpl(
0 /* host_free */, stats,
0 /* host_available */, false, "test"));
}
// Test that we deflate the balloon even if we just need a small piece.
{
const BalloonStats stats = {.disk_caches = 0,
.free_memory = policy.MaxFree() * 3 / 4};
EXPECT_EQ(
-(policy.MaxFree() / 4),
policy.ComputeBalloonDeltaImpl(0 /* host_free */, stats,
0 /* host_available */, false, "test"));
}
}
// Tests that moderate_target_cache works as expected.
TEST(BalloonPolicyTest, LimitCacheModerate) {
// Values are roughly what a 4GB ARCVM would get (but rounded)
const int64_t host_lwm = 200 * MIB;
ZoneInfoStats guest_stats = {.sum_low = 200 * MIB, .totalreserve = 300 * MIB};
MemoryMargins margins = {.critical = 400 * MIB, .moderate = 2000 * MIB};
const LimitCacheBalloonPolicy::Params params = {
.reclaim_target_cache = 0,
.critical_target_cache = 0,
.moderate_target_cache = 200 * MIB};
LimitCacheBalloonPolicy policy(margins, host_lwm, guest_stats, params,
"test");
// limit_start is the host_available level below which we start limiting
// guest memory.
const uint64_t limit_start =
margins.moderate + policy.MaxFree() - guest_stats.sum_low;
// Test that we inflate the balloon a bit when we start getting a bit close
// to the moderate margin.
{
BalloonStats stats = {.disk_caches = 1000 * MIB,
.free_memory = policy.MaxFree()};
EXPECT_EQ(MIB, policy.ComputeBalloonDeltaImpl(
0 /* host_free */, stats,
limit_start - MIB /* host_available */, false, "test"));
}
// Test that when there is less cache left than the distance to target free,
// we only inflate the balloon enough to reclaim that cache.
{
BalloonStats stats = {.disk_caches = 300 * MIB,
.free_memory = policy.MaxFree()};
const int64_t cache_above_limit =
stats.disk_caches - params.moderate_target_cache;
EXPECT_EQ(cache_above_limit,
policy.ComputeBalloonDeltaImpl(
0 /* host_free */, stats,
margins.moderate /* host_available */, false, "test"));
}
// Test that when we are way below the moderate margin, we still give the
// guest MinFree() memory.
{
BalloonStats stats = {.disk_caches = 2000 * MIB,
.free_memory = policy.MaxFree()};
const int64_t free_above_min = stats.free_memory - policy.MinFree();
EXPECT_EQ(free_above_min, policy.ComputeBalloonDeltaImpl(
0 /* host_free */, stats,
0 /* host_available */, false, "test"));
}
}
// Tests that critical_target_cache works as expected.
TEST(BalloonPolicyTest, LimitCacheCritical) {
// Values are roughly what a 4GB ARCVM would get (but rounded)
const int64_t host_lwm = 200 * MIB;
ZoneInfoStats guest_stats = {.sum_low = 200 * MIB, .totalreserve = 300 * MIB};
MemoryMargins margins = {.critical = 400 * MIB, .moderate = 2000 * MIB};
const LimitCacheBalloonPolicy::Params params = {
.reclaim_target_cache = 0,
.critical_target_cache = 100 * MIB,
.moderate_target_cache = 0};
LimitCacheBalloonPolicy policy(margins, host_lwm, guest_stats, params,
"test");
// limit_start is the host_available level below which we start limiting
// guest memory.
const uint64_t limit_start =
margins.critical + policy.MaxFree() - guest_stats.sum_low;
// Test that we inflate the balloon a bit when we start getting a bit close
// to the critical margin.
{
BalloonStats stats = {.disk_caches = 1000 * MIB,
.free_memory = policy.MaxFree()};
EXPECT_EQ(MIB, policy.ComputeBalloonDeltaImpl(
0 /* host_free */, stats,
limit_start - MIB /* host_available */, false, "test"));
}
// Test that when there is less cache left than the distance to target free,
// we only inflate the balloon enough to reclaim that cache.
{
BalloonStats stats = {.disk_caches = 150 * MIB,
.free_memory = policy.MaxFree()};
const int64_t cache_above_limit =
stats.disk_caches - params.critical_target_cache;
EXPECT_EQ(cache_above_limit,
policy.ComputeBalloonDeltaImpl(
0 /* host_free */, stats,
margins.critical /* host_available */, false, "test"));
}
// Test that when we are way below the critical margin, we still give the
// guest MinFree() memory.
{
BalloonStats stats = {.disk_caches = 1000 * MIB,
.free_memory = policy.MaxFree()};
const int64_t free_above_min = stats.free_memory - policy.MinFree();
EXPECT_EQ(free_above_min, policy.ComputeBalloonDeltaImpl(
0 /* host_free */, stats,
0 /* host_available */, false, "test"));
}
}
// Tests that reclaim_target_cache works as expected.
TEST(BalloonPolicyTest, LimitCacheReclaim) {
// Values are roughly what a 4GB ARCVM would get (but rounded)
const int64_t host_lwm = 200 * MIB;
ZoneInfoStats guest_stats = {.sum_low = 200 * MIB, .totalreserve = 300 * MIB};
MemoryMargins margins = {.critical = 400 * MIB, .moderate = 2000 * MIB};
const LimitCacheBalloonPolicy::Params params = {
.reclaim_target_cache = 100 * MIB,
.critical_target_cache = 0,
.moderate_target_cache = 0};
LimitCacheBalloonPolicy policy(margins, host_lwm, guest_stats, params,
"test");
// limit_start is the host_free level below which we start limiting
// guest memory.
const uint64_t limit_start =
host_lwm + policy.MaxFree() - guest_stats.sum_low;
// Test that we inflate the balloon a bit when we start getting a bit close
// to reclaiming in the host.
{
BalloonStats stats = {.disk_caches = 1000 * MIB,
.free_memory = policy.MaxFree()};
EXPECT_EQ(MIB, policy.ComputeBalloonDeltaImpl(
limit_start - MIB /* host_free */, stats,
0 /* host_available */, false, "test"));
}
// Test that when there is less cache left than the distance to target free,
// we only inflate the balloon enough to reclaim that cache.
{
BalloonStats stats = {.disk_caches = 150 * MIB,
.free_memory = policy.MaxFree()};
const int64_t cache_above_limit =
stats.disk_caches - params.reclaim_target_cache;
EXPECT_EQ(cache_above_limit, policy.ComputeBalloonDeltaImpl(
host_lwm /* host_free */, stats,
0 /* host_available */, false, "test"));
}
// Test that when we are way past reclaiming in the host, we still give the
// guest MinFree() memory.
{
BalloonStats stats = {.disk_caches = 1000 * MIB,
.free_memory = policy.MaxFree()};
const int64_t free_above_min = stats.free_memory - policy.MinFree();
EXPECT_EQ(free_above_min, policy.ComputeBalloonDeltaImpl(
0 /* host_free */, stats,
0 /* host_available */, false, "test"));
}
}
// Tests that critical_target_cache and moderate_target_cache work together as
// expected.
TEST(BalloonPolicyTest, LimitCacheModerateAndCritical) {
// Values are roughly what a 4GB ARCVM would get (but rounded)
const int64_t host_lwm = 200 * MIB;
ZoneInfoStats guest_stats = {.sum_low = 200 * MIB, .totalreserve = 300 * MIB};
MemoryMargins margins = {.critical = 400 * MIB, .moderate = 2000 * MIB};
const LimitCacheBalloonPolicy::Params params = {
.reclaim_target_cache = 0,
.critical_target_cache = 100 * MIB,
.moderate_target_cache = 200 * MIB};
LimitCacheBalloonPolicy policy(margins, host_lwm, guest_stats, params,
"test");
// Test that when we are limited by both moderate and critical available cache
// limits, the smaller of the two is used.
BalloonStats stats = {.disk_caches = 150 * MIB,
.free_memory = policy.MaxFree()};
const int64_t cache_above_limit =
stats.disk_caches - params.critical_target_cache;
EXPECT_EQ(cache_above_limit,
policy.ComputeBalloonDeltaImpl(
0 /* host_free */, stats, margins.critical /* host_available */,
false, "test"));
}
// Tests that the guest gets MinFree memory even if the host is very low.
TEST(BalloonPolicyTest, LimitCacheGuestFreeLow) {
// Values are roughly what a 4GB ARCVM would get (but rounded)
const int64_t host_lwm = 200 * MIB;
ZoneInfoStats guest_stats = {.sum_low = 200 * MIB, .totalreserve = 300 * MIB};
MemoryMargins margins = {.critical = 400 * MIB, .moderate = 2000 * MIB};
const LimitCacheBalloonPolicy::Params params = {
.reclaim_target_cache = 0,
.critical_target_cache = 100 * MIB,
.moderate_target_cache = 200 * MIB};
LimitCacheBalloonPolicy policy(margins, host_lwm, guest_stats, params,
"test");
BalloonStats stats = {.disk_caches = 150 * MIB, .free_memory = 0};
EXPECT_EQ(-policy.MinFree(), policy.ComputeBalloonDeltaImpl(
0 /* host_free */, stats,
0 /* host_available */, false, "test"));
}
// Tests that ParseZoneInfoStats works on real input.
TEST(BalloonPolicyTest, ParseZoneInfoStatsSnapshot) {
auto stats = ParseZoneInfoStats(
"Node 0, zone DMA\n"
" per-node stats\n"
" nr_inactive_anon 364023\n"
" nr_active_anon 97740\n"
" nr_inactive_file 20238\n"
" nr_active_file 95809\n"
" nr_unevictable 24263\n"
" nr_slab_reclaimable 7997\n"
" nr_slab_unreclaimable 18546\n"
" nr_isolated_anon 0\n"
" nr_isolated_file 0\n"
" workingset_nodes 1789\n"
" workingset_refault_anon 0\n"
" workingset_refault_file 86864\n"
" workingset_activate_anon 0\n"
" workingset_activate_file 13430\n"
" workingset_restore_anon 0\n"
" workingset_restore_file 72672\n"
" workingset_nodereclaim 0\n"
" nr_anon_pages 450240\n"
" nr_mapped 48448\n"
" nr_file_pages 140275\n"
" nr_dirty 0\n"
" nr_writeback 0\n"
" nr_writeback_temp 0\n"
" nr_shmem 23504\n"
" nr_shmem_hugepages 0\n"
" nr_shmem_pmdmapped 0\n"
" nr_file_hugepages 0\n"
" nr_file_pmdmapped 0\n"
" nr_anon_transparent_hugepages 123\n"
" nr_vmscan_write 0\n"
" nr_vmscan_immediate_reclaim 0\n"
" nr_dirtied 95963\n"
" nr_written 95960\n"
" nr_kernel_misc_reclaimable 0\n"
" nr_foll_pin_acquired 392\n"
" nr_foll_pin_released 392\n"
" nr_kernel_stack 17440\n"
" pages free 3208\n"
" min 113\n"
" low 151\n"
" high 179\n"
" spanned 4095\n"
" present 3742\n"
" managed 3208\n"
" cma 0\n"
" protection: (0, 3248, 3700, 3700, 3700)\n"
" nr_free_pages 3208\n"
" nr_zone_inactive_anon 0\n"
" nr_zone_active_anon 0\n"
" nr_zone_inactive_file 0\n"
" nr_zone_active_file 0\n"
" nr_zone_unevictable 0\n"
" nr_zone_write_pending 0\n"
" nr_mlock 0\n"
" nr_page_table_pages 0\n"
" nr_bounce 0\n"
" nr_zspages 0\n"
" nr_free_cma 0\n"
" pagesets\n"
" cpu: 0\n"
" count: 0\n"
" high: 0\n"
" batch: 1\n"
" vm stats threshold: 4\n"
" cpu: 1\n"
" count: 0\n"
" high: 0\n"
" batch: 1\n"
" vm stats threshold: 4\n"
" cpu: 2\n"
" count: 0\n"
" high: 0\n"
" batch: 1\n"
" vm stats threshold: 4\n"
" node_unreclaimable: 0\n"
" start_pfn: 1\n"
"Node 0, zone DMA32\n"
" pages free 55144\n"
" min 29527\n"
" low 39744\n"
" high 47125\n"
" spanned 1044480\n"
" present 847872\n"
" managed 831488\n"
" cma 0\n"
" protection: (0, 0, 452, 452, 452)\n"
" nr_free_pages 55144\n"
" nr_zone_inactive_anon 299032\n"
" nr_zone_active_anon 87931\n"
" nr_zone_inactive_file 19179\n"
" nr_zone_active_file 86754\n"
" nr_zone_unevictable 20737\n"
" nr_zone_write_pending 0\n"
" nr_mlock 21\n"
" nr_page_table_pages 7964\n"
" nr_bounce 0\n"
" nr_zspages 0\n"
" nr_free_cma 0\n"
" pagesets\n"
" cpu: 0\n"
" count: 58\n"
" high: 378\n"
" batch: 63\n"
" vm stats threshold: 24\n"
" cpu: 1\n"
" count: 95\n"
" high: 378\n"
" batch: 63\n"
" vm stats threshold: 24\n"
" cpu: 2\n"
" count: 0\n"
" high: 378\n"
" batch: 63\n"
" vm stats threshold: 24\n"
" node_unreclaimable: 0\n"
" start_pfn: 4096\n"
"Node 0, zone Normal\n"
" pages free 7002\n"
" min 4150\n"
" low 5586\n"
" high 6623\n"
" spanned 141824\n"
" present 141824\n"
" managed 116890\n"
" cma 0\n"
" protection: (0, 0, 0, 0, 0)\n"
" nr_free_pages 7002\n"
" nr_zone_inactive_anon 64991\n"
" nr_zone_active_anon 9801\n"
" nr_zone_inactive_file 1059\n"
" nr_zone_active_file 9055\n"
" nr_zone_unevictable 3526\n"
" nr_zone_write_pending 0\n"
" nr_mlock 1892\n"
" nr_page_table_pages 839\n"
" nr_bounce 0\n"
" nr_zspages 0\n"
" nr_free_cma 0\n"
" pagesets\n"
" cpu: 0\n"
" count: 41\n"
" high: 186\n"
" batch: 31\n"
" vm stats threshold: 12\n"
" cpu: 1\n"
" count: 7\n"
" high: 186\n"
" batch: 31\n"
" vm stats threshold: 12\n"
" cpu: 2\n"
" count: 0\n"
" high: 186\n"
" batch: 31\n"
" vm stats threshold: 12\n"
" node_unreclaimable: 0\n"
" start_pfn: 1048576\n"
"Node 0, zone Movable\n"
" pages free 0\n"
" min 0\n"
" low 0\n"
" high 0\n"
" spanned 0\n"
" present 0\n"
" managed 0\n"
" cma 0\n"
" protection: (0, 0, 0, 0, 0)\n"
"Node 0, zone Device\n"
" pages free 0\n"
" min 0\n"
" low 0\n"
" high 0\n"
" spanned 0\n"
" present 0\n"
" managed 0\n"
" cma 0\n"
" protection: (0, 0, 0, 0, 0)\n");
EXPECT_TRUE(stats);
EXPECT_EQ(stats->sum_low, 45481 * PAGE_BYTES);
EXPECT_EQ(stats->totalreserve, 85041 * PAGE_BYTES);
}
// Tests that ParseZoneInfoStats works on real input.
TEST(BalloonPolicyTest, ParseZoneInfoStatsFailures) {
EXPECT_FALSE(ParseZoneInfoStats(""));
// Missing non-zero high and protection.
EXPECT_FALSE(ParseZoneInfoStats("low 1"));
// Missing protection.
EXPECT_FALSE(ParseZoneInfoStats("low 1\nhigh 1"));
// Bad low watermark.
EXPECT_FALSE(ParseZoneInfoStats("low 1a\nhigh 1\nprotection(1)"));
// Bad low watermark.
EXPECT_FALSE(ParseZoneInfoStats("low 1 1\nhigh 1\nprotection(1)"));
// Bad high watermark.
EXPECT_FALSE(ParseZoneInfoStats("low 1\nhigh a1\nprotection(1)"));
// Bad high watermark.
EXPECT_FALSE(ParseZoneInfoStats("low 1\nhigh 2 2\nprotection(1)"));
// Missing low.
EXPECT_FALSE(ParseZoneInfoStats("high 1\nprotection: (1)"));
// Missing high before protection.
EXPECT_FALSE(ParseZoneInfoStats("low 1\nprotection: (1)"));
// Missing high before protection.
EXPECT_FALSE(
ParseZoneInfoStats("low 1\nhigh 1\nprotection: (1)\nprotection: (1)"));
// No protection line between two high lines.
EXPECT_FALSE(ParseZoneInfoStats("low 1\nhigh 1\nhigh: 1"));
}
} // namespace concierge
} // namespace vm_tools