1*d166453eSJames Houghton // SPDX-License-Identifier: GPL-2.0-only 2*d166453eSJames Houghton /* 3*d166453eSJames Houghton * Copyright (C) 2025, Google LLC. 4*d166453eSJames Houghton */ 5*d166453eSJames Houghton 6*d166453eSJames Houghton #include <time.h> 7*d166453eSJames Houghton 8*d166453eSJames Houghton #include "lru_gen_util.h" 9*d166453eSJames Houghton 10*d166453eSJames Houghton /* 11*d166453eSJames Houghton * Tracks state while we parse memcg lru_gen stats. The file we're parsing is 12*d166453eSJames Houghton * structured like this (some extra whitespace elided): 13*d166453eSJames Houghton * 14*d166453eSJames Houghton * memcg (id) (path) 15*d166453eSJames Houghton * node (id) 16*d166453eSJames Houghton * (gen_nr) (age_in_ms) (nr_anon_pages) (nr_file_pages) 17*d166453eSJames Houghton */ 18*d166453eSJames Houghton struct memcg_stats_parse_context { 19*d166453eSJames Houghton bool consumed; /* Whether or not this line was consumed */ 20*d166453eSJames Houghton /* Next parse handler to invoke */ 21*d166453eSJames Houghton void (*next_handler)(struct memcg_stats *stats, 22*d166453eSJames Houghton struct memcg_stats_parse_context *ctx, 23*d166453eSJames Houghton char *line); 24*d166453eSJames Houghton int current_node_idx; /* Current index in nodes array */ 25*d166453eSJames Houghton const char *name; /* The name of the memcg we're looking for */ 26*d166453eSJames Houghton }; 27*d166453eSJames Houghton 28*d166453eSJames Houghton static void memcg_stats_handle_searching(struct memcg_stats *stats, 29*d166453eSJames Houghton struct memcg_stats_parse_context *ctx, 30*d166453eSJames Houghton char *line); 31*d166453eSJames Houghton static void memcg_stats_handle_in_memcg(struct memcg_stats *stats, 32*d166453eSJames Houghton struct memcg_stats_parse_context *ctx, 33*d166453eSJames Houghton char *line); 34*d166453eSJames Houghton static void memcg_stats_handle_in_node(struct memcg_stats *stats, 35*d166453eSJames Houghton struct memcg_stats_parse_context *ctx, 36*d166453eSJames Houghton char *line); 37*d166453eSJames Houghton 38*d166453eSJames Houghton struct split_iterator { 39*d166453eSJames Houghton char *str; 40*d166453eSJames Houghton char *save; 41*d166453eSJames Houghton }; 42*d166453eSJames Houghton 43*d166453eSJames Houghton static char *split_next(struct split_iterator *it) 44*d166453eSJames Houghton { 45*d166453eSJames Houghton char *ret = strtok_r(it->str, " \t\n\r", &it->save); 46*d166453eSJames Houghton 47*d166453eSJames Houghton it->str = NULL; 48*d166453eSJames Houghton return ret; 49*d166453eSJames Houghton } 50*d166453eSJames Houghton 51*d166453eSJames Houghton static void memcg_stats_handle_searching(struct memcg_stats *stats, 52*d166453eSJames Houghton struct memcg_stats_parse_context *ctx, 53*d166453eSJames Houghton char *line) 54*d166453eSJames Houghton { 55*d166453eSJames Houghton struct split_iterator it = { .str = line }; 56*d166453eSJames Houghton char *prefix = split_next(&it); 57*d166453eSJames Houghton char *memcg_id = split_next(&it); 58*d166453eSJames Houghton char *memcg_name = split_next(&it); 59*d166453eSJames Houghton char *end; 60*d166453eSJames Houghton 61*d166453eSJames Houghton ctx->consumed = true; 62*d166453eSJames Houghton 63*d166453eSJames Houghton if (!prefix || strcmp("memcg", prefix)) 64*d166453eSJames Houghton return; /* Not a memcg line (maybe empty), skip */ 65*d166453eSJames Houghton 66*d166453eSJames Houghton TEST_ASSERT(memcg_id && memcg_name, 67*d166453eSJames Houghton "malformed memcg line; no memcg id or memcg_name"); 68*d166453eSJames Houghton 69*d166453eSJames Houghton if (strcmp(memcg_name + 1, ctx->name)) 70*d166453eSJames Houghton return; /* Wrong memcg, skip */ 71*d166453eSJames Houghton 72*d166453eSJames Houghton /* Found it! */ 73*d166453eSJames Houghton 74*d166453eSJames Houghton stats->memcg_id = strtoul(memcg_id, &end, 10); 75*d166453eSJames Houghton TEST_ASSERT(*end == '\0', "malformed memcg id '%s'", memcg_id); 76*d166453eSJames Houghton if (!stats->memcg_id) 77*d166453eSJames Houghton return; /* Removed memcg? */ 78*d166453eSJames Houghton 79*d166453eSJames Houghton ctx->next_handler = memcg_stats_handle_in_memcg; 80*d166453eSJames Houghton } 81*d166453eSJames Houghton 82*d166453eSJames Houghton static void memcg_stats_handle_in_memcg(struct memcg_stats *stats, 83*d166453eSJames Houghton struct memcg_stats_parse_context *ctx, 84*d166453eSJames Houghton char *line) 85*d166453eSJames Houghton { 86*d166453eSJames Houghton struct split_iterator it = { .str = line }; 87*d166453eSJames Houghton char *prefix = split_next(&it); 88*d166453eSJames Houghton char *id = split_next(&it); 89*d166453eSJames Houghton long found_node_id; 90*d166453eSJames Houghton char *end; 91*d166453eSJames Houghton 92*d166453eSJames Houghton ctx->consumed = true; 93*d166453eSJames Houghton ctx->current_node_idx = -1; 94*d166453eSJames Houghton 95*d166453eSJames Houghton if (!prefix) 96*d166453eSJames Houghton return; /* Skip empty lines */ 97*d166453eSJames Houghton 98*d166453eSJames Houghton if (!strcmp("memcg", prefix)) { 99*d166453eSJames Houghton /* Memcg done, found next one; stop. */ 100*d166453eSJames Houghton ctx->next_handler = NULL; 101*d166453eSJames Houghton return; 102*d166453eSJames Houghton } else if (strcmp("node", prefix)) 103*d166453eSJames Houghton TEST_ASSERT(false, "found malformed line after 'memcg ...'," 104*d166453eSJames Houghton "token: '%s'", prefix); 105*d166453eSJames Houghton 106*d166453eSJames Houghton /* At this point we know we have a node line. Parse the ID. */ 107*d166453eSJames Houghton 108*d166453eSJames Houghton TEST_ASSERT(id, "malformed node line; no node id"); 109*d166453eSJames Houghton 110*d166453eSJames Houghton found_node_id = strtol(id, &end, 10); 111*d166453eSJames Houghton TEST_ASSERT(*end == '\0', "malformed node id '%s'", id); 112*d166453eSJames Houghton 113*d166453eSJames Houghton ctx->current_node_idx = stats->nr_nodes++; 114*d166453eSJames Houghton TEST_ASSERT(ctx->current_node_idx < MAX_NR_NODES, 115*d166453eSJames Houghton "memcg has stats for too many nodes, max is %d", 116*d166453eSJames Houghton MAX_NR_NODES); 117*d166453eSJames Houghton stats->nodes[ctx->current_node_idx].node = found_node_id; 118*d166453eSJames Houghton 119*d166453eSJames Houghton ctx->next_handler = memcg_stats_handle_in_node; 120*d166453eSJames Houghton } 121*d166453eSJames Houghton 122*d166453eSJames Houghton static void memcg_stats_handle_in_node(struct memcg_stats *stats, 123*d166453eSJames Houghton struct memcg_stats_parse_context *ctx, 124*d166453eSJames Houghton char *line) 125*d166453eSJames Houghton { 126*d166453eSJames Houghton char *my_line = strdup(line); 127*d166453eSJames Houghton struct split_iterator it = { .str = my_line }; 128*d166453eSJames Houghton char *gen, *age, *nr_anon, *nr_file; 129*d166453eSJames Houghton struct node_stats *node_stats; 130*d166453eSJames Houghton struct generation_stats *gen_stats; 131*d166453eSJames Houghton char *end; 132*d166453eSJames Houghton 133*d166453eSJames Houghton TEST_ASSERT(it.str, "failed to copy input line"); 134*d166453eSJames Houghton 135*d166453eSJames Houghton gen = split_next(&it); 136*d166453eSJames Houghton 137*d166453eSJames Houghton if (!gen) 138*d166453eSJames Houghton goto out_consume; /* Skip empty lines */ 139*d166453eSJames Houghton 140*d166453eSJames Houghton if (!strcmp("memcg", gen) || !strcmp("node", gen)) { 141*d166453eSJames Houghton /* 142*d166453eSJames Houghton * Reached next memcg or node section. Don't consume, let the 143*d166453eSJames Houghton * other handler deal with this. 144*d166453eSJames Houghton */ 145*d166453eSJames Houghton ctx->next_handler = memcg_stats_handle_in_memcg; 146*d166453eSJames Houghton goto out; 147*d166453eSJames Houghton } 148*d166453eSJames Houghton 149*d166453eSJames Houghton node_stats = &stats->nodes[ctx->current_node_idx]; 150*d166453eSJames Houghton TEST_ASSERT(node_stats->nr_gens < MAX_NR_GENS, 151*d166453eSJames Houghton "found too many generation lines; max is %d", 152*d166453eSJames Houghton MAX_NR_GENS); 153*d166453eSJames Houghton gen_stats = &node_stats->gens[node_stats->nr_gens++]; 154*d166453eSJames Houghton 155*d166453eSJames Houghton age = split_next(&it); 156*d166453eSJames Houghton nr_anon = split_next(&it); 157*d166453eSJames Houghton nr_file = split_next(&it); 158*d166453eSJames Houghton 159*d166453eSJames Houghton TEST_ASSERT(age && nr_anon && nr_file, 160*d166453eSJames Houghton "malformed generation line; not enough tokens"); 161*d166453eSJames Houghton 162*d166453eSJames Houghton gen_stats->gen = (int)strtol(gen, &end, 10); 163*d166453eSJames Houghton TEST_ASSERT(*end == '\0', "malformed generation number '%s'", gen); 164*d166453eSJames Houghton 165*d166453eSJames Houghton gen_stats->age_ms = strtol(age, &end, 10); 166*d166453eSJames Houghton TEST_ASSERT(*end == '\0', "malformed generation age '%s'", age); 167*d166453eSJames Houghton 168*d166453eSJames Houghton gen_stats->nr_anon = strtol(nr_anon, &end, 10); 169*d166453eSJames Houghton TEST_ASSERT(*end == '\0', "malformed anonymous page count '%s'", 170*d166453eSJames Houghton nr_anon); 171*d166453eSJames Houghton 172*d166453eSJames Houghton gen_stats->nr_file = strtol(nr_file, &end, 10); 173*d166453eSJames Houghton TEST_ASSERT(*end == '\0', "malformed file page count '%s'", nr_file); 174*d166453eSJames Houghton 175*d166453eSJames Houghton out_consume: 176*d166453eSJames Houghton ctx->consumed = true; 177*d166453eSJames Houghton out: 178*d166453eSJames Houghton free(my_line); 179*d166453eSJames Houghton } 180*d166453eSJames Houghton 181*d166453eSJames Houghton static void print_memcg_stats(const struct memcg_stats *stats, const char *name) 182*d166453eSJames Houghton { 183*d166453eSJames Houghton int node, gen; 184*d166453eSJames Houghton 185*d166453eSJames Houghton pr_debug("stats for memcg %s (id %lu):\n", name, stats->memcg_id); 186*d166453eSJames Houghton for (node = 0; node < stats->nr_nodes; ++node) { 187*d166453eSJames Houghton pr_debug("\tnode %d\n", stats->nodes[node].node); 188*d166453eSJames Houghton for (gen = 0; gen < stats->nodes[node].nr_gens; ++gen) { 189*d166453eSJames Houghton const struct generation_stats *gstats = 190*d166453eSJames Houghton &stats->nodes[node].gens[gen]; 191*d166453eSJames Houghton 192*d166453eSJames Houghton pr_debug("\t\tgen %d\tage_ms %ld" 193*d166453eSJames Houghton "\tnr_anon %ld\tnr_file %ld\n", 194*d166453eSJames Houghton gstats->gen, gstats->age_ms, gstats->nr_anon, 195*d166453eSJames Houghton gstats->nr_file); 196*d166453eSJames Houghton } 197*d166453eSJames Houghton } 198*d166453eSJames Houghton } 199*d166453eSJames Houghton 200*d166453eSJames Houghton /* Re-read lru_gen debugfs information for @memcg into @stats. */ 201*d166453eSJames Houghton void lru_gen_read_memcg_stats(struct memcg_stats *stats, const char *memcg) 202*d166453eSJames Houghton { 203*d166453eSJames Houghton FILE *f; 204*d166453eSJames Houghton ssize_t read = 0; 205*d166453eSJames Houghton char *line = NULL; 206*d166453eSJames Houghton size_t bufsz; 207*d166453eSJames Houghton struct memcg_stats_parse_context ctx = { 208*d166453eSJames Houghton .next_handler = memcg_stats_handle_searching, 209*d166453eSJames Houghton .name = memcg, 210*d166453eSJames Houghton }; 211*d166453eSJames Houghton 212*d166453eSJames Houghton memset(stats, 0, sizeof(struct memcg_stats)); 213*d166453eSJames Houghton 214*d166453eSJames Houghton f = fopen(LRU_GEN_DEBUGFS, "r"); 215*d166453eSJames Houghton TEST_ASSERT(f, "fopen(%s) failed", LRU_GEN_DEBUGFS); 216*d166453eSJames Houghton 217*d166453eSJames Houghton while (ctx.next_handler && (read = getline(&line, &bufsz, f)) > 0) { 218*d166453eSJames Houghton ctx.consumed = false; 219*d166453eSJames Houghton 220*d166453eSJames Houghton do { 221*d166453eSJames Houghton ctx.next_handler(stats, &ctx, line); 222*d166453eSJames Houghton if (!ctx.next_handler) 223*d166453eSJames Houghton break; 224*d166453eSJames Houghton } while (!ctx.consumed); 225*d166453eSJames Houghton } 226*d166453eSJames Houghton 227*d166453eSJames Houghton if (read < 0 && !feof(f)) 228*d166453eSJames Houghton TEST_ASSERT(false, "getline(%s) failed", LRU_GEN_DEBUGFS); 229*d166453eSJames Houghton 230*d166453eSJames Houghton TEST_ASSERT(stats->memcg_id > 0, "Couldn't find memcg: %s\n" 231*d166453eSJames Houghton "Did the memcg get created in the proper mount?", 232*d166453eSJames Houghton memcg); 233*d166453eSJames Houghton if (line) 234*d166453eSJames Houghton free(line); 235*d166453eSJames Houghton TEST_ASSERT(!fclose(f), "fclose(%s) failed", LRU_GEN_DEBUGFS); 236*d166453eSJames Houghton 237*d166453eSJames Houghton print_memcg_stats(stats, memcg); 238*d166453eSJames Houghton } 239*d166453eSJames Houghton 240*d166453eSJames Houghton /* 241*d166453eSJames Houghton * Find all pages tracked by lru_gen for this memcg in generation @target_gen. 242*d166453eSJames Houghton * 243*d166453eSJames Houghton * If @target_gen is negative, look for all generations. 244*d166453eSJames Houghton */ 245*d166453eSJames Houghton long lru_gen_sum_memcg_stats_for_gen(int target_gen, 246*d166453eSJames Houghton const struct memcg_stats *stats) 247*d166453eSJames Houghton { 248*d166453eSJames Houghton int node, gen; 249*d166453eSJames Houghton long total_nr = 0; 250*d166453eSJames Houghton 251*d166453eSJames Houghton for (node = 0; node < stats->nr_nodes; ++node) { 252*d166453eSJames Houghton const struct node_stats *node_stats = &stats->nodes[node]; 253*d166453eSJames Houghton 254*d166453eSJames Houghton for (gen = 0; gen < node_stats->nr_gens; ++gen) { 255*d166453eSJames Houghton const struct generation_stats *gen_stats = 256*d166453eSJames Houghton &node_stats->gens[gen]; 257*d166453eSJames Houghton 258*d166453eSJames Houghton if (target_gen >= 0 && gen_stats->gen != target_gen) 259*d166453eSJames Houghton continue; 260*d166453eSJames Houghton 261*d166453eSJames Houghton total_nr += gen_stats->nr_anon + gen_stats->nr_file; 262*d166453eSJames Houghton } 263*d166453eSJames Houghton } 264*d166453eSJames Houghton 265*d166453eSJames Houghton return total_nr; 266*d166453eSJames Houghton } 267*d166453eSJames Houghton 268*d166453eSJames Houghton /* Find all pages tracked by lru_gen for this memcg. */ 269*d166453eSJames Houghton long lru_gen_sum_memcg_stats(const struct memcg_stats *stats) 270*d166453eSJames Houghton { 271*d166453eSJames Houghton return lru_gen_sum_memcg_stats_for_gen(-1, stats); 272*d166453eSJames Houghton } 273*d166453eSJames Houghton 274*d166453eSJames Houghton /* 275*d166453eSJames Houghton * If lru_gen aging should force page table scanning. 276*d166453eSJames Houghton * 277*d166453eSJames Houghton * If you want to set this to false, you will need to do eviction 278*d166453eSJames Houghton * before doing extra aging passes. 279*d166453eSJames Houghton */ 280*d166453eSJames Houghton static const bool force_scan = true; 281*d166453eSJames Houghton 282*d166453eSJames Houghton static void run_aging_impl(unsigned long memcg_id, int node_id, int max_gen) 283*d166453eSJames Houghton { 284*d166453eSJames Houghton FILE *f = fopen(LRU_GEN_DEBUGFS, "w"); 285*d166453eSJames Houghton char *command; 286*d166453eSJames Houghton size_t sz; 287*d166453eSJames Houghton 288*d166453eSJames Houghton TEST_ASSERT(f, "fopen(%s) failed", LRU_GEN_DEBUGFS); 289*d166453eSJames Houghton sz = asprintf(&command, "+ %lu %d %d 1 %d\n", 290*d166453eSJames Houghton memcg_id, node_id, max_gen, force_scan); 291*d166453eSJames Houghton TEST_ASSERT(sz > 0, "creating aging command failed"); 292*d166453eSJames Houghton 293*d166453eSJames Houghton pr_debug("Running aging command: %s", command); 294*d166453eSJames Houghton if (fwrite(command, sizeof(char), sz, f) < sz) { 295*d166453eSJames Houghton TEST_ASSERT(false, "writing aging command %s to %s failed", 296*d166453eSJames Houghton command, LRU_GEN_DEBUGFS); 297*d166453eSJames Houghton } 298*d166453eSJames Houghton 299*d166453eSJames Houghton TEST_ASSERT(!fclose(f), "fclose(%s) failed", LRU_GEN_DEBUGFS); 300*d166453eSJames Houghton } 301*d166453eSJames Houghton 302*d166453eSJames Houghton void lru_gen_do_aging(struct memcg_stats *stats, const char *memcg) 303*d166453eSJames Houghton { 304*d166453eSJames Houghton int node, gen; 305*d166453eSJames Houghton 306*d166453eSJames Houghton pr_debug("lru_gen: invoking aging...\n"); 307*d166453eSJames Houghton 308*d166453eSJames Houghton /* Must read memcg stats to construct the proper aging command. */ 309*d166453eSJames Houghton lru_gen_read_memcg_stats(stats, memcg); 310*d166453eSJames Houghton 311*d166453eSJames Houghton for (node = 0; node < stats->nr_nodes; ++node) { 312*d166453eSJames Houghton int max_gen = 0; 313*d166453eSJames Houghton 314*d166453eSJames Houghton for (gen = 0; gen < stats->nodes[node].nr_gens; ++gen) { 315*d166453eSJames Houghton int this_gen = stats->nodes[node].gens[gen].gen; 316*d166453eSJames Houghton 317*d166453eSJames Houghton max_gen = max_gen > this_gen ? max_gen : this_gen; 318*d166453eSJames Houghton } 319*d166453eSJames Houghton 320*d166453eSJames Houghton run_aging_impl(stats->memcg_id, stats->nodes[node].node, 321*d166453eSJames Houghton max_gen); 322*d166453eSJames Houghton } 323*d166453eSJames Houghton 324*d166453eSJames Houghton /* Re-read so callers get updated information */ 325*d166453eSJames Houghton lru_gen_read_memcg_stats(stats, memcg); 326*d166453eSJames Houghton } 327*d166453eSJames Houghton 328*d166453eSJames Houghton /* 329*d166453eSJames Houghton * Find which generation contains at least @pages pages, assuming that 330*d166453eSJames Houghton * such a generation exists. 331*d166453eSJames Houghton */ 332*d166453eSJames Houghton int lru_gen_find_generation(const struct memcg_stats *stats, 333*d166453eSJames Houghton unsigned long pages) 334*d166453eSJames Houghton { 335*d166453eSJames Houghton int node, gen, gen_idx, min_gen = INT_MAX, max_gen = -1; 336*d166453eSJames Houghton 337*d166453eSJames Houghton for (node = 0; node < stats->nr_nodes; ++node) 338*d166453eSJames Houghton for (gen_idx = 0; gen_idx < stats->nodes[node].nr_gens; 339*d166453eSJames Houghton ++gen_idx) { 340*d166453eSJames Houghton gen = stats->nodes[node].gens[gen_idx].gen; 341*d166453eSJames Houghton max_gen = gen > max_gen ? gen : max_gen; 342*d166453eSJames Houghton min_gen = gen < min_gen ? gen : min_gen; 343*d166453eSJames Houghton } 344*d166453eSJames Houghton 345*d166453eSJames Houghton for (gen = min_gen; gen <= max_gen; ++gen) 346*d166453eSJames Houghton /* See if this generation has enough pages. */ 347*d166453eSJames Houghton if (lru_gen_sum_memcg_stats_for_gen(gen, stats) > pages) 348*d166453eSJames Houghton return gen; 349*d166453eSJames Houghton 350*d166453eSJames Houghton return -1; 351*d166453eSJames Houghton } 352*d166453eSJames Houghton 353*d166453eSJames Houghton bool lru_gen_usable(void) 354*d166453eSJames Houghton { 355*d166453eSJames Houghton long required_features = LRU_GEN_ENABLED | LRU_GEN_MM_WALK; 356*d166453eSJames Houghton int lru_gen_fd, lru_gen_debug_fd; 357*d166453eSJames Houghton char mglru_feature_str[8] = {}; 358*d166453eSJames Houghton long mglru_features; 359*d166453eSJames Houghton 360*d166453eSJames Houghton lru_gen_fd = open(LRU_GEN_ENABLED_PATH, O_RDONLY); 361*d166453eSJames Houghton if (lru_gen_fd < 0) { 362*d166453eSJames Houghton puts("lru_gen: Could not open " LRU_GEN_ENABLED_PATH); 363*d166453eSJames Houghton return false; 364*d166453eSJames Houghton } 365*d166453eSJames Houghton if (read(lru_gen_fd, &mglru_feature_str, 7) < 7) { 366*d166453eSJames Houghton puts("lru_gen: Could not read from " LRU_GEN_ENABLED_PATH); 367*d166453eSJames Houghton close(lru_gen_fd); 368*d166453eSJames Houghton return false; 369*d166453eSJames Houghton } 370*d166453eSJames Houghton close(lru_gen_fd); 371*d166453eSJames Houghton 372*d166453eSJames Houghton mglru_features = strtol(mglru_feature_str, NULL, 16); 373*d166453eSJames Houghton if ((mglru_features & required_features) != required_features) { 374*d166453eSJames Houghton printf("lru_gen: missing features, got: 0x%lx, expected: 0x%lx\n", 375*d166453eSJames Houghton mglru_features, required_features); 376*d166453eSJames Houghton printf("lru_gen: Try 'echo 0x%lx > /sys/kernel/mm/lru_gen/enabled'\n", 377*d166453eSJames Houghton required_features); 378*d166453eSJames Houghton return false; 379*d166453eSJames Houghton } 380*d166453eSJames Houghton 381*d166453eSJames Houghton lru_gen_debug_fd = open(LRU_GEN_DEBUGFS, O_RDWR); 382*d166453eSJames Houghton __TEST_REQUIRE(lru_gen_debug_fd >= 0, 383*d166453eSJames Houghton "lru_gen: Could not open " LRU_GEN_DEBUGFS ", " 384*d166453eSJames Houghton "but lru_gen is enabled, so cannot use page_idle."); 385*d166453eSJames Houghton close(lru_gen_debug_fd); 386*d166453eSJames Houghton return true; 387*d166453eSJames Houghton } 388