Re: memcg stats flushing (WAS "Re: [PATCH] fuse: disable default bdi strictlimiting")
James Houghton <[email protected]>
| Newsgroups | dev.linux.lists.fuse-devel,org.kernel.vger.cgroups,org.kernel.vger.linux-fsdevel,org.kvack.linux-mm |
|---|---|
| Message-ID | <CADrL8HV7_f68qfd6hG=xCTqzupPqr77ynbwdz8jm0hzjdgDVeg@mail.gmail.com> |
On Fri, Aug 21, 2026 at 12:41 PM Yosry Ahmed <[email protected]> wrote: > > On Thu, Aug 20, 2026 at 4:39 PM Joanne Koong <[email protected]> wrote: > > Would you be able to share the test you're using? I've been trying to > > repro it locally without much luck. > > The exact test uses some internal testing libraries, we're looking > into whether we can translate it to a standalone C repro. > > From a high-level, the test: > - Creates a VM with 260 MB (256 MB for testing + 4 MB margin) in a > cgroup, backed by shmem THPs. The VM is not a fully fledged VM, just a > KVM VM running bare metal code (so 4 MB is enough margin). > - Enables zswap in the cgroup. > - Tells the VM to write to 256 MB of its memory. > - Checks the cgroup shmem usage and that no swap occurred (no limit). > - Sets the cgroup limit to half the VM memory. > - Checks the cgroup shmem usage and swap again, usage should be ~half > and so is swap. > - Tells the VM to read the 256 MB of memory again. This should cause a > lot of thrashing and reclaim, but no OOM kills as all guest memory is > reclaimable. > > With the ratelimited flushing, the test started flaking 5-10% of the > time due to OOM kills. Hi Joanne, Yosry, I've attached a reproducer for you (as a patch that should apply to Linus's tree). In tools/testing/selftests/kvm, `make` should build it. Instructions for running it (like setting up swap/etc.) are in the comment at the top of the file. As part of the patch, I've also included a sysctl to dynamically tune whether or not the memcg stats flush is rate-limited. You should find that when it is rate-limited, OOMs occur, and when it is not, OOMs do not occur. This reproducer behaves slightly differently than our internal version. This version, when run with `-b 512M` anyway, seems to OOM simply by setting memory.max, whereas our internal version would OOM upon re-accessing the memory. Not really a meaningful difference, but just wanted to point it out. Please let me know if you have any trouble running this test or if the test is wrong in some way. Thanks!
0001-KVM-selftests-reclaim_test.patch
(application/x-patch, 11.5 KB)
From ec781e28885540ab89162e3d67c98759c0c9b2f6 Mon Sep 17 00:00:00 2001 From: James Houghton <[email protected]> Date: Sat, 22 Aug 2026 00:30:18 +0000 Subject: [PATCH] KVM: selftests: reclaim_test --- mm/vmscan.c | 21 +- tools/testing/selftests/kvm/Makefile.kvm | 1 + tools/testing/selftests/kvm/reclaim_test.c | 334 +++++++++++++++++++++ 3 files changed, 355 insertions(+), 1 deletion(-) create mode 100644 tools/testing/selftests/kvm/reclaim_test.c diff --git a/mm/vmscan.c b/mm/vmscan.c index 4ca9775ceee8..069c3422754e 100644 --- a/mm/vmscan.c +++ b/mm/vmscan.c @@ -2273,6 +2273,13 @@ enum scan_balance { SCAN_FILE, }; +/* + * Values: + * 0 flush stats synchronously + * 1 (default) flush stats ratelimited (upstream default) + */ +static int sysctl_memcg_flush_stats_ratelimited __read_mostly = 1; + static void prepare_scan_control(pg_data_t *pgdat, struct scan_control *sc) { unsigned long file; @@ -2288,7 +2295,10 @@ static void prepare_scan_control(pg_data_t *pgdat, struct scan_control *sc) * most accurate stats here. We may switch to regular stats flushing * in the future once it is cheap enough. */ - mem_cgroup_flush_stats_ratelimited(sc->target_mem_cgroup); + if (READ_ONCE(sysctl_memcg_flush_stats_ratelimited)) + mem_cgroup_flush_stats_ratelimited(sc->target_mem_cgroup); + else + mem_cgroup_flush_stats(sc->target_mem_cgroup); /* * Determine the scan balance between anon and file LRUs. @@ -7659,6 +7669,15 @@ static const struct ctl_table vmscan_sysctl_table[] = { .extra1 = SYSCTL_ZERO, .extra2 = SYSCTL_TWO_HUNDRED, }, + { + .procname = "memcg_flush_stats_ratelimited", + .data = &sysctl_memcg_flush_stats_ratelimited, + .maxlen = sizeof(sysctl_memcg_flush_stats_ratelimited), + .mode = 0644, + .proc_handler = proc_dointvec_minmax, + .extra1 = SYSCTL_ZERO, + .extra2 = SYSCTL_ONE, + }, #ifdef CONFIG_NUMA { .procname = "zone_reclaim_mode", diff --git a/tools/testing/selftests/kvm/Makefile.kvm b/tools/testing/selftests/kvm/Makefile.kvm index 6fc34e9bf8e1..a2ca1a6e7b8c 100644 --- a/tools/testing/selftests/kvm/Makefile.kvm +++ b/tools/testing/selftests/kvm/Makefile.kvm @@ -66,6 +66,7 @@ TEST_GEN_PROGS_COMMON += kvm_page_table_test TEST_GEN_PROGS_COMMON += set_memory_region_test TEST_GEN_PROGS_COMMON += memslot_modification_stress_test TEST_GEN_PROGS_COMMON += memslot_perf_test +TEST_GEN_PROGS_COMMON += reclaim_test # Compiled test targets TEST_GEN_PROGS_x86 = $(TEST_GEN_PROGS_COMMON) diff --git a/tools/testing/selftests/kvm/reclaim_test.c b/tools/testing/selftests/kvm/reclaim_test.c new file mode 100644 index 000000000000..80cf74343f5c --- /dev/null +++ b/tools/testing/selftests/kvm/reclaim_test.c @@ -0,0 +1,334 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * reclaim_test + * + * Copyright (C) 2026, Google, Inc. + * Based on access_tracking_perf_test. + * + * Test reclaim behavior with a KVM VM. The test creates a memcg, creates a + * memstress VM, and lowers memory.max to induce reclaim. + * + * echo 1 > /sys/module/zswap/parameters/enabled + * echo "+memory" > /sys/fs/cgroup/cgroup.subtree_control + * dd if=/dev/zero of=swapfile bs=1M count=1K + * mkswap swapfile + * swapon swapfile + * + * ./reclaim_test -b 512M + * + * sysctl vm.memcg_flush_stats_ratelimited=0 => passes + * sysctl vm.memcg_flush_stats_ratelimited=1 => fails + */ +#include <inttypes.h> +#include <limits.h> +#include <pthread.h> +#include <sys/mman.h> +#include <sys/types.h> +#include <sys/stat.h> + +#include "kvm_util.h" +#include "test_util.h" +#include "memstress.h" +#include "guest_modes.h" +#include "processor.h" +#include "ucall_common.h" + +#include "cgroup_util.h" +#include "lru_gen_util.h" + +static const char *TEST_MEMCG_NAME = "reclaim_test"; + +/* Global variable used to synchronize all of the vCPU threads. */ +static int iteration; + +/* The cgroup memory controller root. Needed for lru_gen-based aging. */ +char cgroup_root[PATH_MAX]; + +/* Defines what vCPU threads should do during a given iteration. */ +static enum { + /* Run the vCPU to access all its memory. */ + ITERATION_ACCESS_MEMORY, +} iteration_work; + +/* The iteration that was last completed by each vCPU. */ +static int vcpu_last_completed_iteration[KVM_MAX_VCPUS]; + +/* Whether to overlap the regions of memory vCPUs access. */ +static bool overlap_memory_access; + +/* Total number of pages to expect in the memcg after touching everything */ +static long test_pages; + +struct test_params { + /* The backing source for the region of memory. */ + enum vm_mem_backing_src_type backing_src; + + /* The amount of memory to allocate for each vCPU. */ + uint64_t vcpu_memory_bytes; + + /* The number of vCPUs to create in the VM. */ + int nr_vcpus; +}; + +static void assert_ucall(struct kvm_vcpu *vcpu, uint64_t expected_ucall) +{ + struct ucall uc; + uint64_t actual_ucall = get_ucall(vcpu, &uc); + + TEST_ASSERT(expected_ucall == actual_ucall, + "Guest exited unexpectedly (expected ucall %" PRIu64 + ", got %" PRIu64 ")", + expected_ucall, actual_ucall); +} + +static bool spin_wait_for_next_iteration(int *current_iteration) +{ + int last_iteration = *current_iteration; + + do { + if (READ_ONCE(memstress_args.stop_vcpus)) + return false; + + *current_iteration = READ_ONCE(iteration); + } while (last_iteration == *current_iteration); + + return true; +} + +static void vcpu_thread_main(struct memstress_vcpu_args *vcpu_args) +{ + struct kvm_vcpu *vcpu = vcpu_args->vcpu; + /* struct kvm_vm *vm = memstress_args.vm; */ + int vcpu_idx = vcpu_args->vcpu_idx; + int current_iteration = 0; + + while (spin_wait_for_next_iteration(¤t_iteration)) { + switch (READ_ONCE(iteration_work)) { + case ITERATION_ACCESS_MEMORY: + vcpu_run(vcpu); + assert_ucall(vcpu, UCALL_SYNC); + break; + } + + vcpu_last_completed_iteration[vcpu_idx] = current_iteration; + } +} + +static void spin_wait_for_vcpu(int vcpu_idx, int target_iteration) +{ + while (READ_ONCE(vcpu_last_completed_iteration[vcpu_idx]) != + target_iteration) { + continue; + } +} + +/* The type of memory accesses to perform in the VM. */ +enum access_type { + ACCESS_READ, + ACCESS_WRITE, +}; + +static void run_iteration(struct kvm_vm *vm, int nr_vcpus, const char *description) +{ + struct timespec ts_start; + struct timespec ts_elapsed; + int next_iteration, i; + + /* Kick off the vCPUs by incrementing iteration. */ + next_iteration = ++iteration; + + clock_gettime(CLOCK_MONOTONIC, &ts_start); + + /* Wait for all vCPUs to finish the iteration. */ + for (i = 0; i < nr_vcpus; i++) + spin_wait_for_vcpu(i, next_iteration); + + ts_elapsed = timespec_elapsed(ts_start); + pr_info("%-30s: %ld.%09lds\n", + description, ts_elapsed.tv_sec, ts_elapsed.tv_nsec); +} + +static void access_memory(struct kvm_vm *vm, int nr_vcpus, + enum access_type access, const char *description) +{ + memstress_set_write_percent(vm, (access == ACCESS_READ) ? 0 : 100); + iteration_work = ITERATION_ACCESS_MEMORY; + run_iteration(vm, nr_vcpus, description); +} + +#define MEM_MAX "memory.max" +#define MEM_CURRENT "memory.current" +#define MEM_SWAP_CURRENT "memory.swap.current" + +// #define MEM_MAX "memory.limit_in_bytes" +// #define MEM_CURRENT "memory.usage_in_bytes" +// #define MEM_SWAP_CURRENT "memory.memsw.usage_in_bytes" + +static void run_test(enum vm_guest_mode mode, void *arg) +{ + struct test_params *params = arg; + struct kvm_vm *vm; + char buf[64]; + char *cg; + int nr_vcpus = params->nr_vcpus; + + cg = cg_name(cgroup_root, TEST_MEMCG_NAME); + + /* Reset memory.max so we don't immediately OOM. */ + if (cg_write(cg, MEM_MAX, "9223372036854771712")) { + printf(MEM_MAX " -> 'max' failed\n"); + return; + } + + vm = memstress_create_vm(mode, nr_vcpus, params->vcpu_memory_bytes, 1, + params->backing_src, !overlap_memory_access); + + /* + * If guest_page_size is larger than the host's page size, the + * guest (memstress) will only fault in a subset of the host's pages. + */ + test_pages = params->nr_vcpus * params->vcpu_memory_bytes / + max(memstress_args.guest_page_size, + (uint64_t)getpagesize()); + + memstress_start_vcpu_threads(nr_vcpus, vcpu_thread_main); + + pr_info("\n"); + access_memory(vm, nr_vcpus, ACCESS_WRITE, "Populating memory"); + + if (cg_read(cg, MEM_CURRENT, buf, sizeof(buf))) { + printf("couldn't read " MEM_CURRENT "\n"); + return; + } + else + printf(MEM_CURRENT ": %s\n", buf); + + unsigned long bytes_used = strtoull(buf, NULL, 10); + unsigned long target_bytes = bytes_used / 2; + + + printf("Adjusting " MEM_MAX "...\n"); + snprintf(buf, sizeof(buf), "%ld", target_bytes); + if (cg_write(cg, MEM_MAX, buf)) { + printf("couldn't write " MEM_MAX ", errno=%d\n", errno); + return; + } + + if (cg_read(cg, MEM_CURRENT, buf, sizeof(buf))) { + printf("couldn't read " MEM_CURRENT "\n"); + return; + } + printf(MEM_CURRENT ": %s", buf); + + if (cg_read(cg, MEM_SWAP_CURRENT, buf, sizeof(buf))) { + printf("couldn't read " MEM_SWAP_CURRENT "\n"); + return; + } + printf(MEM_SWAP_CURRENT ": %s", buf); + + for (int i = 0; i < 50; ++i) { + access_memory(vm, nr_vcpus, ACCESS_READ, "Reading from populated memory"); + } + + memstress_join_vcpu_threads(nr_vcpus); + memstress_destroy_vm(vm); +} + +static int run_test_for_each_guest_mode(const char *cgroup, void *arg) +{ + for_each_guest_mode(run_test, arg); + return 0; +} + +static void help(char *name) +{ + puts(""); + printf("usage: %s [-h] [-m mode] [-b vcpu_bytes] [-v vcpus] [-o] [-s mem_type]\n", + name); + puts(""); + printf(" -h: Display this help message."); + guest_modes_help(); + 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"); + printf(" -v: specify the number of vCPUs to run.\n"); + printf(" -o: Overlap guest memory accesses instead of partitioning\n" + " them into a separate region of memory for each vCPU.\n"); + backing_src_help("-s"); + puts(""); + exit(0); +} + +void destroy_cgroup(char *cg) +{ + printf("Destroying cgroup: %s\n", cg); +} + +int main(int argc, char *argv[]) +{ + struct test_params params = { + .backing_src = DEFAULT_VM_MEM_SRC, + .vcpu_memory_bytes = DEFAULT_PER_VCPU_MEM_SIZE, + .nr_vcpus = 1, + }; + char *new_cg = NULL; + int opt; + + guest_modes_append_default(); + + while ((opt = getopt(argc, argv, "hm:b:v:os:w:")) != -1) { + switch (opt) { + case 'm': + guest_modes_cmdline(optarg); + break; + case 'b': + params.vcpu_memory_bytes = parse_size(optarg); + break; + case 'v': + params.nr_vcpus = atoi_positive("Number of vCPUs", optarg); + break; + case 'o': + overlap_memory_access = true; + break; + case 's': + params.backing_src = parse_backing_src_type(optarg); + break; + case 'h': + default: + help(argv[0]); + break; + } + } + + bool cg_created = true; + int ret; + + if (cg_find_controller_root(cgroup_root, sizeof(cgroup_root), "memory")) + ksft_exit_skip("Cannot find memory cgroup controller\n"); + + new_cg = cg_name(cgroup_root, TEST_MEMCG_NAME); + printf("Creating cgroup: %s\n", new_cg); + if (cg_create(new_cg)) { + if (errno == EEXIST) { + printf("Found existing cgroup\n"); + cg_created = false; + } else { + ksft_exit_skip("could not create new cgroup: %s\n", new_cg); + } + } + + /* + * This will fork off a new process to run the test within + * a new memcg, so we need to properly propagate the return + * value up. + */ + ret = cg_run(new_cg, &run_test_for_each_guest_mode, ¶ms); + if (cg_created) + cg_destroy(new_cg); + if (ret < 0) + TEST_FAIL("child did not spawn or was abnormally killed"); + if (ret) + return ret; + + return 0; +} base-commit: 26260251022fbc2f248a3d747a9b2b961b18d2d8 -- 2.55.0.766.g2966f0265a-goog