Contributors: 3
Author Tokens Token Proportion Commits Commit Proportion
Ben Gardon 965 94.24% 1 33.33%
Sean Christopherson 53 5.18% 1 33.33%
Thomas Huth 6 0.59% 1 33.33%
Total 1024 3


// SPDX-License-Identifier: GPL-2.0
/*
 * KVM dirty logging page splitting test
 *
 * Based on dirty_log_perf.c
 *
 * Copyright (C) 2018, Red Hat, Inc.
 * Copyright (C) 2023, Google, Inc.
 */

#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <linux/bitmap.h>

#include "kvm_util.h"
#include "test_util.h"
#include "memstress.h"
#include "guest_modes.h"

#define VCPUS		2
#define SLOTS		2
#define ITERATIONS	2

static uint64_t guest_percpu_mem_size = DEFAULT_PER_VCPU_MEM_SIZE;

static enum vm_mem_backing_src_type backing_src = VM_MEM_SRC_ANONYMOUS_HUGETLB;

static u64 dirty_log_manual_caps;
static bool host_quit;
static int iteration;
static int vcpu_last_completed_iteration[KVM_MAX_VCPUS];

struct kvm_page_stats {
	uint64_t pages_4k;
	uint64_t pages_2m;
	uint64_t pages_1g;
	uint64_t hugepages;
};

static void get_page_stats(struct kvm_vm *vm, struct kvm_page_stats *stats, const char *stage)
{
	stats->pages_4k = vm_get_stat(vm, "pages_4k");
	stats->pages_2m = vm_get_stat(vm, "pages_2m");
	stats->pages_1g = vm_get_stat(vm, "pages_1g");
	stats->hugepages = stats->pages_2m + stats->pages_1g;

	pr_debug("\nPage stats after %s: 4K: %ld 2M: %ld 1G: %ld huge: %ld\n",
		 stage, stats->pages_4k, stats->pages_2m, stats->pages_1g,
		 stats->hugepages);
}

static void run_vcpu_iteration(struct kvm_vm *vm)
{
	int i;

	iteration++;
	for (i = 0; i < VCPUS; i++) {
		while (READ_ONCE(vcpu_last_completed_iteration[i]) !=
		       iteration)
			;
	}
}

static void vcpu_worker(struct memstress_vcpu_args *vcpu_args)
{
	struct kvm_vcpu *vcpu = vcpu_args->vcpu;
	int vcpu_idx = vcpu_args->vcpu_idx;

	while (!READ_ONCE(host_quit)) {
		int current_iteration = READ_ONCE(iteration);

		vcpu_run(vcpu);

		TEST_ASSERT_EQ(get_ucall(vcpu, NULL), UCALL_SYNC);

		vcpu_last_completed_iteration[vcpu_idx] = current_iteration;

		/* Wait for the start of the next iteration to be signaled. */
		while (current_iteration == READ_ONCE(iteration) &&
		       READ_ONCE(iteration) >= 0 &&
		       !READ_ONCE(host_quit))
			;
	}
}

static void run_test(enum vm_guest_mode mode, void *unused)
{
	struct kvm_vm *vm;
	unsigned long **bitmaps;
	uint64_t guest_num_pages;
	uint64_t host_num_pages;
	uint64_t pages_per_slot;
	int i;
	struct kvm_page_stats stats_populated;
	struct kvm_page_stats stats_dirty_logging_enabled;
	struct kvm_page_stats stats_dirty_pass[ITERATIONS];
	struct kvm_page_stats stats_clear_pass[ITERATIONS];
	struct kvm_page_stats stats_dirty_logging_disabled;
	struct kvm_page_stats stats_repopulated;

	vm = memstress_create_vm(mode, VCPUS, guest_percpu_mem_size,
				 SLOTS, backing_src, false);

	guest_num_pages = (VCPUS * guest_percpu_mem_size) >> vm->page_shift;
	guest_num_pages = vm_adjust_num_guest_pages(mode, guest_num_pages);
	host_num_pages = vm_num_host_pages(mode, guest_num_pages);
	pages_per_slot = host_num_pages / SLOTS;
	TEST_ASSERT_EQ(host_num_pages, pages_per_slot * SLOTS);
	TEST_ASSERT(!(host_num_pages % 512),
		    "Number of pages, '%lu' not a multiple of 2MiB", host_num_pages);

	bitmaps = memstress_alloc_bitmaps(SLOTS, pages_per_slot);

	if (dirty_log_manual_caps)
		vm_enable_cap(vm, KVM_CAP_MANUAL_DIRTY_LOG_PROTECT2,
			      dirty_log_manual_caps);

	/* Start the iterations */
	iteration = -1;
	host_quit = false;

	for (i = 0; i < VCPUS; i++)
		vcpu_last_completed_iteration[i] = -1;

	memstress_start_vcpu_threads(VCPUS, vcpu_worker);

	run_vcpu_iteration(vm);
	get_page_stats(vm, &stats_populated, "populating memory");

	/* Enable dirty logging */
	memstress_enable_dirty_logging(vm, SLOTS);

	get_page_stats(vm, &stats_dirty_logging_enabled, "enabling dirty logging");

	while (iteration < ITERATIONS) {
		run_vcpu_iteration(vm);
		get_page_stats(vm, &stats_dirty_pass[iteration - 1],
			       "dirtying memory");

		memstress_get_dirty_log(vm, bitmaps, SLOTS);

		if (dirty_log_manual_caps) {
			memstress_clear_dirty_log(vm, bitmaps, SLOTS, pages_per_slot);

			get_page_stats(vm, &stats_clear_pass[iteration - 1], "clearing dirty log");
		}
	}

	/* Disable dirty logging */
	memstress_disable_dirty_logging(vm, SLOTS);

	get_page_stats(vm, &stats_dirty_logging_disabled, "disabling dirty logging");

	/* Run vCPUs again to fault pages back in. */
	run_vcpu_iteration(vm);
	get_page_stats(vm, &stats_repopulated, "repopulating memory");

	/*
	 * Tell the vCPU threads to quit.  No need to manually check that vCPUs
	 * have stopped running after disabling dirty logging, the join will
	 * wait for them to exit.
	 */
	host_quit = true;
	memstress_join_vcpu_threads(VCPUS);

	memstress_free_bitmaps(bitmaps, SLOTS);
	memstress_destroy_vm(vm);

	TEST_ASSERT_EQ((stats_populated.pages_2m * 512 +
			stats_populated.pages_1g * 512 * 512), host_num_pages);

	/*
	 * Check that all huge pages were split. Since large pages can only
	 * exist in the data slot, and the vCPUs should have dirtied all pages
	 * in the data slot, there should be no huge pages left after splitting.
	 * Splitting happens at dirty log enable time without
	 * KVM_CAP_MANUAL_DIRTY_LOG_PROTECT2 and after the first clear pass
	 * with that capability.
	 */
	if (dirty_log_manual_caps) {
		TEST_ASSERT_EQ(stats_clear_pass[0].hugepages, 0);
		TEST_ASSERT(stats_clear_pass[0].pages_4k >= host_num_pages,
			    "Expected at least '%lu' 4KiB pages, found only '%lu'",
			    host_num_pages, stats_clear_pass[0].pages_4k);
		TEST_ASSERT_EQ(stats_dirty_logging_enabled.hugepages, stats_populated.hugepages);
	} else {
		TEST_ASSERT_EQ(stats_dirty_logging_enabled.hugepages, 0);
		TEST_ASSERT(stats_dirty_logging_enabled.pages_4k >= host_num_pages,
			    "Expected at least '%lu' 4KiB pages, found only '%lu'",
			    host_num_pages, stats_dirty_logging_enabled.pages_4k);
	}

	/*
	 * Once dirty logging is disabled and the vCPUs have touched all their
	 * memory again, the hugepage counts should be the same as they were
	 * right after initial population of memory.
	 */
	TEST_ASSERT_EQ(stats_populated.pages_2m, stats_repopulated.pages_2m);
	TEST_ASSERT_EQ(stats_populated.pages_1g, stats_repopulated.pages_1g);
}

static void help(char *name)
{
	puts("");
	printf("usage: %s [-h] [-b vcpu bytes] [-s mem type]\n",
	       name);
	puts("");
	printf(" -b: specify the size of the memory region which should be\n"
	       "     dirtied by each vCPU. e.g. 10M or 3G.\n"
	       "     (default: 1G)\n");
	backing_src_help("-s");
	puts("");
}

int main(int argc, char *argv[])
{
	int opt;

	TEST_REQUIRE(get_kvm_param_bool("eager_page_split"));
	TEST_REQUIRE(get_kvm_param_bool("tdp_mmu"));

	while ((opt = getopt(argc, argv, "b:hs:")) != -1) {
		switch (opt) {
		case 'b':
			guest_percpu_mem_size = parse_size(optarg);
			break;
		case 'h':
			help(argv[0]);
			exit(0);
		case 's':
			backing_src = parse_backing_src_type(optarg);
			break;
		default:
			help(argv[0]);
			exit(1);
		}
	}

	if (!is_backing_src_hugetlb(backing_src)) {
		pr_info("This test will only work reliably with HugeTLB memory. "
			"It can work with THP, but that is best effort.\n");
	}

	guest_modes_append_default();

	dirty_log_manual_caps = 0;
	for_each_guest_mode(run_test, NULL);

	dirty_log_manual_caps =
		kvm_check_cap(KVM_CAP_MANUAL_DIRTY_LOG_PROTECT2);

	if (dirty_log_manual_caps) {
		dirty_log_manual_caps &= (KVM_DIRTY_LOG_MANUAL_PROTECT_ENABLE |
					  KVM_DIRTY_LOG_INITIALLY_SET);
		for_each_guest_mode(run_test, NULL);
	} else {
		pr_info("Skipping testing with MANUAL_PROTECT as it is not supported");
	}

	return 0;
}