[RFC v2 3/3] mm/lru_gen: expose oldest-generation page counts in memory.stat
From: Zicheng Wang
Date: Tue Jul 14 2026 - 08:25:37 EST
Add nr_oldest_anon and nr_oldest_file to memory.stat: pages in the
oldest generation (min_seq) of each type, aggregated over the cgroup
subtree and all N_MEMORY nodes. This is the observation half of the
proactive-aging loop - it shows a policy how much anon/file memory the
next reclaim can evict directly, so it can decide whether aging is
needed.
Computed on demand from lrugen->nr_pages; no per-page maintenance, no
hot-path cost. Paired with the AGING counter and workingset_refault_*
to confirm an aging pass took effect and was not too aggressive.
Gated by CONFIG_LRU_GEN; reported in pages, matching the lru_gen dump.
Signed-off-by: Zicheng Wang <wangzicheng@xxxxxxxxx>
---
Documentation/admin-guide/cgroup-v2.rst | 18 ++++++++++++++++
mm/internal.h | 2 ++
mm/memcontrol.c | 24 +++++++++++++++++++++
mm/vmscan.c | 28 +++++++++++++++++++++++++
4 files changed, 72 insertions(+)
diff --git a/Documentation/admin-guide/cgroup-v2.rst b/Documentation/admin-guide/cgroup-v2.rst
index f42ac6dddbf6..a071175d11be 100644
--- a/Documentation/admin-guide/cgroup-v2.rst
+++ b/Documentation/admin-guide/cgroup-v2.rst
@@ -1658,6 +1658,24 @@ The following nested keys are defined.
the value for the foo counter, since the foo counter is type-based, not
list-based.
+ nr_oldest_anon, nr_oldest_file
+ Number of pages in the oldest generation of the anonymous and
+ file types. Only present when CONFIG_LRU_GEN is enabled.
+
+ With MGLRU, page reclaim starts from the oldest generation
+ (min_seq) of each type, so these counts show how much anonymous
+ and file memory the next reclaim pass (or a proactive
+ memory.reclaim) can directly evict. They are aggregated across
+ this cgroup's subtree and all NUMA nodes, and are reported in
+ pages, matching the per-generation breakdown in the ``lru_gen``
+ debugfs file.
+
+ Proactive-aging policy can use them together with the ``aging``
+ counter and ``workingset_refault_*``: a large ``nr_oldest_file``
+ with near-zero ``nr_oldest_anon`` indicates file cache piling up
+ in the oldest generation while anonymous pages stay young, which
+ is the condition ``memory.aging`` is meant to rebalance.
+
slab_reclaimable
Part of "slab" that might be reclaimed, such as
dentries and inodes.
diff --git a/mm/internal.h b/mm/internal.h
index 96add6cd4627..959d376c02a6 100644
--- a/mm/internal.h
+++ b/mm/internal.h
@@ -636,6 +636,8 @@ int user_proactive_reclaim(char *buf,
#ifdef CONFIG_LRU_GEN
int lru_gen_age_memcg(struct mem_cgroup *memcg, unsigned long nr_gens);
+void lru_gen_nr_oldest_pages(struct lruvec *lruvec,
+ unsigned long *nr_anon, unsigned long *nr_file);
#endif
/*
diff --git a/mm/memcontrol.c b/mm/memcontrol.c
index c6a969b56878..016194cabd60 100644
--- a/mm/memcontrol.c
+++ b/mm/memcontrol.c
@@ -1733,6 +1733,30 @@ static void memcg_stat_format(struct mem_cgroup *memcg, struct seq_buf *s)
}
}
+#ifdef CONFIG_LRU_GEN
+ /* Oldest-generation (min_seq) anon/file pages over this cgroup's
+ * subtree and all N_MEMORY nodes; see lru_gen_nr_oldest_pages().
+ */
+ {
+ struct mem_cgroup *mi;
+ int nid;
+ unsigned long oldest_anon = 0, oldest_file = 0;
+
+ for_each_mem_cgroup_tree(mi, memcg) {
+ for_each_node_state(nid, N_MEMORY) {
+ unsigned long nr_anon, nr_file;
+ struct lruvec *lruvec = mem_cgroup_lruvec(mi, NODE_DATA(nid));
+
+ lru_gen_nr_oldest_pages(lruvec, &nr_anon, &nr_file);
+ oldest_anon += nr_anon;
+ oldest_file += nr_file;
+ }
+ }
+ seq_buf_printf(s, "nr_oldest_anon %lu\n", oldest_anon);
+ seq_buf_printf(s, "nr_oldest_file %lu\n", oldest_file);
+ }
+#endif
+
/* Accumulated memory events */
seq_buf_printf(s, "pgscan %lu\n",
memcg_page_state(memcg, PGSCAN_KSWAPD) +
diff --git a/mm/vmscan.c b/mm/vmscan.c
index 805e29c499c8..3800f0c4f1f5 100644
--- a/mm/vmscan.c
+++ b/mm/vmscan.c
@@ -5935,6 +5935,34 @@ int lru_gen_age_memcg(struct mem_cgroup *memcg, unsigned long nr_gens)
return ret;
}
+/**
+ * lru_gen_nr_oldest_pages - pages in the oldest generation of a lruvec
+ * @lruvec: target lruvec
+ * @nr_anon: filled with oldest-generation anonymous page count
+ * @nr_file: filled with oldest-generation file page count
+ *
+ * Reclaim starts from the oldest generation (min_seq) of each type, so these
+ * count what the next reclaim can evict directly. Computed from
+ * lrugen->nr_pages[min_seq type][zone]; eventually consistent, clamped to 0.
+ */
+void lru_gen_nr_oldest_pages(struct lruvec *lruvec,
+ unsigned long *nr_anon, unsigned long *nr_file)
+{
+ int type, zone;
+ struct lru_gen_folio *lrugen = &lruvec->lrugen;
+ unsigned long size[ANON_AND_FILE] = {};
+
+ for (type = 0; type < ANON_AND_FILE; type++) {
+ int gen = lru_gen_from_seq(READ_ONCE(lrugen->min_seq[type]));
+
+ for (zone = 0; zone < MAX_NR_ZONES; zone++)
+ size[type] += max(READ_ONCE(lrugen->nr_pages[gen][type][zone]), 0L);
+ }
+
+ *nr_anon = size[LRU_GEN_ANON];
+ *nr_file = size[LRU_GEN_FILE];
+}
+
#else /* !CONFIG_LRU_GEN */
static void lru_gen_age_node(struct pglist_data *pgdat, struct scan_control *sc)
--
2.25.1