Path: blob/master/tools/testing/selftests/kvm/lib/lru_gen_util.c
38235 views
// SPDX-License-Identifier: GPL-2.0-only1/*2* Copyright (C) 2025, Google LLC.3*/45#include <time.h>67#include "lru_gen_util.h"89/*10* Tracks state while we parse memcg lru_gen stats. The file we're parsing is11* structured like this (some extra whitespace elided):12*13* memcg (id) (path)14* node (id)15* (gen_nr) (age_in_ms) (nr_anon_pages) (nr_file_pages)16*/17struct memcg_stats_parse_context {18bool consumed; /* Whether or not this line was consumed */19/* Next parse handler to invoke */20void (*next_handler)(struct memcg_stats *stats,21struct memcg_stats_parse_context *ctx,22char *line);23int current_node_idx; /* Current index in nodes array */24const char *name; /* The name of the memcg we're looking for */25};2627static void memcg_stats_handle_searching(struct memcg_stats *stats,28struct memcg_stats_parse_context *ctx,29char *line);30static void memcg_stats_handle_in_memcg(struct memcg_stats *stats,31struct memcg_stats_parse_context *ctx,32char *line);33static void memcg_stats_handle_in_node(struct memcg_stats *stats,34struct memcg_stats_parse_context *ctx,35char *line);3637struct split_iterator {38char *str;39char *save;40};4142static char *split_next(struct split_iterator *it)43{44char *ret = strtok_r(it->str, " \t\n\r", &it->save);4546it->str = NULL;47return ret;48}4950static void memcg_stats_handle_searching(struct memcg_stats *stats,51struct memcg_stats_parse_context *ctx,52char *line)53{54struct split_iterator it = { .str = line };55char *prefix = split_next(&it);56char *memcg_id = split_next(&it);57char *memcg_name = split_next(&it);58char *end;5960ctx->consumed = true;6162if (!prefix || strcmp("memcg", prefix))63return; /* Not a memcg line (maybe empty), skip */6465TEST_ASSERT(memcg_id && memcg_name,66"malformed memcg line; no memcg id or memcg_name");6768if (strcmp(memcg_name + 1, ctx->name))69return; /* Wrong memcg, skip */7071/* Found it! */7273stats->memcg_id = strtoul(memcg_id, &end, 10);74TEST_ASSERT(*end == '\0', "malformed memcg id '%s'", memcg_id);75if (!stats->memcg_id)76return; /* Removed memcg? */7778ctx->next_handler = memcg_stats_handle_in_memcg;79}8081static void memcg_stats_handle_in_memcg(struct memcg_stats *stats,82struct memcg_stats_parse_context *ctx,83char *line)84{85struct split_iterator it = { .str = line };86char *prefix = split_next(&it);87char *id = split_next(&it);88long found_node_id;89char *end;9091ctx->consumed = true;92ctx->current_node_idx = -1;9394if (!prefix)95return; /* Skip empty lines */9697if (!strcmp("memcg", prefix)) {98/* Memcg done, found next one; stop. */99ctx->next_handler = NULL;100return;101} else if (strcmp("node", prefix))102TEST_ASSERT(false, "found malformed line after 'memcg ...',"103"token: '%s'", prefix);104105/* At this point we know we have a node line. Parse the ID. */106107TEST_ASSERT(id, "malformed node line; no node id");108109found_node_id = strtol(id, &end, 10);110TEST_ASSERT(*end == '\0', "malformed node id '%s'", id);111112ctx->current_node_idx = stats->nr_nodes++;113TEST_ASSERT(ctx->current_node_idx < MAX_NR_NODES,114"memcg has stats for too many nodes, max is %d",115MAX_NR_NODES);116stats->nodes[ctx->current_node_idx].node = found_node_id;117118ctx->next_handler = memcg_stats_handle_in_node;119}120121static void memcg_stats_handle_in_node(struct memcg_stats *stats,122struct memcg_stats_parse_context *ctx,123char *line)124{125char *my_line = strdup(line);126struct split_iterator it = { .str = my_line };127char *gen, *age, *nr_anon, *nr_file;128struct node_stats *node_stats;129struct generation_stats *gen_stats;130char *end;131132TEST_ASSERT(it.str, "failed to copy input line");133134gen = split_next(&it);135136if (!gen)137goto out_consume; /* Skip empty lines */138139if (!strcmp("memcg", gen) || !strcmp("node", gen)) {140/*141* Reached next memcg or node section. Don't consume, let the142* other handler deal with this.143*/144ctx->next_handler = memcg_stats_handle_in_memcg;145goto out;146}147148node_stats = &stats->nodes[ctx->current_node_idx];149TEST_ASSERT(node_stats->nr_gens < MAX_NR_GENS,150"found too many generation lines; max is %d",151MAX_NR_GENS);152gen_stats = &node_stats->gens[node_stats->nr_gens++];153154age = split_next(&it);155nr_anon = split_next(&it);156nr_file = split_next(&it);157158TEST_ASSERT(age && nr_anon && nr_file,159"malformed generation line; not enough tokens");160161gen_stats->gen = (int)strtol(gen, &end, 10);162TEST_ASSERT(*end == '\0', "malformed generation number '%s'", gen);163164gen_stats->age_ms = strtol(age, &end, 10);165TEST_ASSERT(*end == '\0', "malformed generation age '%s'", age);166167gen_stats->nr_anon = strtol(nr_anon, &end, 10);168TEST_ASSERT(*end == '\0', "malformed anonymous page count '%s'",169nr_anon);170171gen_stats->nr_file = strtol(nr_file, &end, 10);172TEST_ASSERT(*end == '\0', "malformed file page count '%s'", nr_file);173174out_consume:175ctx->consumed = true;176out:177free(my_line);178}179180static void print_memcg_stats(const struct memcg_stats *stats, const char *name)181{182int node, gen;183184pr_debug("stats for memcg %s (id %lu):\n", name, stats->memcg_id);185for (node = 0; node < stats->nr_nodes; ++node) {186pr_debug("\tnode %d\n", stats->nodes[node].node);187for (gen = 0; gen < stats->nodes[node].nr_gens; ++gen) {188const struct generation_stats *gstats =189&stats->nodes[node].gens[gen];190191pr_debug("\t\tgen %d\tage_ms %ld"192"\tnr_anon %ld\tnr_file %ld\n",193gstats->gen, gstats->age_ms, gstats->nr_anon,194gstats->nr_file);195}196}197}198199/* Re-read lru_gen debugfs information for @memcg into @stats. */200void lru_gen_read_memcg_stats(struct memcg_stats *stats, const char *memcg)201{202FILE *f;203ssize_t read = 0;204char *line = NULL;205size_t bufsz;206struct memcg_stats_parse_context ctx = {207.next_handler = memcg_stats_handle_searching,208.name = memcg,209};210211memset(stats, 0, sizeof(struct memcg_stats));212213f = fopen(LRU_GEN_DEBUGFS, "r");214TEST_ASSERT(f, "fopen(%s) failed", LRU_GEN_DEBUGFS);215216while (ctx.next_handler && (read = getline(&line, &bufsz, f)) > 0) {217ctx.consumed = false;218219do {220ctx.next_handler(stats, &ctx, line);221if (!ctx.next_handler)222break;223} while (!ctx.consumed);224}225226if (read < 0 && !feof(f))227TEST_ASSERT(false, "getline(%s) failed", LRU_GEN_DEBUGFS);228229TEST_ASSERT(stats->memcg_id > 0, "Couldn't find memcg: %s\n"230"Did the memcg get created in the proper mount?",231memcg);232if (line)233free(line);234TEST_ASSERT(!fclose(f), "fclose(%s) failed", LRU_GEN_DEBUGFS);235236print_memcg_stats(stats, memcg);237}238239/*240* Find all pages tracked by lru_gen for this memcg in generation @target_gen.241*242* If @target_gen is negative, look for all generations.243*/244long lru_gen_sum_memcg_stats_for_gen(int target_gen,245const struct memcg_stats *stats)246{247int node, gen;248long total_nr = 0;249250for (node = 0; node < stats->nr_nodes; ++node) {251const struct node_stats *node_stats = &stats->nodes[node];252253for (gen = 0; gen < node_stats->nr_gens; ++gen) {254const struct generation_stats *gen_stats =255&node_stats->gens[gen];256257if (target_gen >= 0 && gen_stats->gen != target_gen)258continue;259260total_nr += gen_stats->nr_anon + gen_stats->nr_file;261}262}263264return total_nr;265}266267/* Find all pages tracked by lru_gen for this memcg. */268long lru_gen_sum_memcg_stats(const struct memcg_stats *stats)269{270return lru_gen_sum_memcg_stats_for_gen(-1, stats);271}272273/*274* If lru_gen aging should force page table scanning.275*276* If you want to set this to false, you will need to do eviction277* before doing extra aging passes.278*/279static const bool force_scan = true;280281static void run_aging_impl(unsigned long memcg_id, int node_id, int max_gen)282{283FILE *f = fopen(LRU_GEN_DEBUGFS, "w");284char *command;285size_t sz;286287TEST_ASSERT(f, "fopen(%s) failed", LRU_GEN_DEBUGFS);288sz = asprintf(&command, "+ %lu %d %d 1 %d\n",289memcg_id, node_id, max_gen, force_scan);290TEST_ASSERT(sz > 0, "creating aging command failed");291292pr_debug("Running aging command: %s", command);293if (fwrite(command, sizeof(char), sz, f) < sz) {294TEST_ASSERT(false, "writing aging command %s to %s failed",295command, LRU_GEN_DEBUGFS);296}297298TEST_ASSERT(!fclose(f), "fclose(%s) failed", LRU_GEN_DEBUGFS);299}300301void lru_gen_do_aging(struct memcg_stats *stats, const char *memcg)302{303int node, gen;304305pr_debug("lru_gen: invoking aging...\n");306307/* Must read memcg stats to construct the proper aging command. */308lru_gen_read_memcg_stats(stats, memcg);309310for (node = 0; node < stats->nr_nodes; ++node) {311int max_gen = 0;312313for (gen = 0; gen < stats->nodes[node].nr_gens; ++gen) {314int this_gen = stats->nodes[node].gens[gen].gen;315316max_gen = max_gen > this_gen ? max_gen : this_gen;317}318319run_aging_impl(stats->memcg_id, stats->nodes[node].node,320max_gen);321}322323/* Re-read so callers get updated information */324lru_gen_read_memcg_stats(stats, memcg);325}326327/*328* Find which generation contains at least @pages pages, assuming that329* such a generation exists.330*/331int lru_gen_find_generation(const struct memcg_stats *stats,332unsigned long pages)333{334int node, gen, gen_idx, min_gen = INT_MAX, max_gen = -1;335336for (node = 0; node < stats->nr_nodes; ++node)337for (gen_idx = 0; gen_idx < stats->nodes[node].nr_gens;338++gen_idx) {339gen = stats->nodes[node].gens[gen_idx].gen;340max_gen = gen > max_gen ? gen : max_gen;341min_gen = gen < min_gen ? gen : min_gen;342}343344for (gen = min_gen; gen <= max_gen; ++gen)345/* See if this generation has enough pages. */346if (lru_gen_sum_memcg_stats_for_gen(gen, stats) > pages)347return gen;348349return -1;350}351352bool lru_gen_usable(void)353{354long required_features = LRU_GEN_ENABLED | LRU_GEN_MM_WALK;355int lru_gen_fd, lru_gen_debug_fd;356char mglru_feature_str[8] = {};357long mglru_features;358359lru_gen_fd = open(LRU_GEN_ENABLED_PATH, O_RDONLY);360if (lru_gen_fd < 0) {361puts("lru_gen: Could not open " LRU_GEN_ENABLED_PATH);362return false;363}364if (read(lru_gen_fd, &mglru_feature_str, 7) < 7) {365puts("lru_gen: Could not read from " LRU_GEN_ENABLED_PATH);366close(lru_gen_fd);367return false;368}369close(lru_gen_fd);370371mglru_features = strtol(mglru_feature_str, NULL, 16);372if ((mglru_features & required_features) != required_features) {373printf("lru_gen: missing features, got: 0x%lx, expected: 0x%lx\n",374mglru_features, required_features);375printf("lru_gen: Try 'echo 0x%lx > /sys/kernel/mm/lru_gen/enabled'\n",376required_features);377return false;378}379380lru_gen_debug_fd = open(LRU_GEN_DEBUGFS, O_RDWR);381__TEST_REQUIRE(lru_gen_debug_fd >= 0,382"lru_gen: Could not open " LRU_GEN_DEBUGFS ", "383"but lru_gen is enabled, so cannot use page_idle.");384close(lru_gen_debug_fd);385return true;386}387388389