All of lore.kernel.org
 help / color / mirror / Atom feed
* [RFC v2 0/3] mm/mglru: proactive aging via memory.aging
@ 2026-07-14 12:15 Zicheng Wang
  2026-07-14 12:15 ` [RFC v2 1/3] mm/lru_gen: add AGING counter and proactive aging helper Zicheng Wang
                   ` (4 more replies)
  0 siblings, 5 replies; 6+ messages in thread
From: Zicheng Wang @ 2026-07-14 12:15 UTC (permalink / raw)
  To: akpm, yuanchu
  Cc: tj, hannes, mkoutny, corbet, kasong, qi.zheng, shakeel.butt,
	baohua, axelrasmussen, weixugc, david, ljs, liam, vbabka, rppt,
	surenb, mhocko, roman.gushchin, muchun.song, cgroups, linux-mm,
	linux-kernel, linux-doc, willy, denghaojie, baoquan.he,
	kaleshsingh, tjmercier, tao.wangtao, zhangji1, wangzhen5,
	Zicheng Wang

MGLRU inverts the reclaim order when anonymous memory is faulted in
bulk: anonymous pages sit in the young generations while file pages
sit in the oldest two, so reclaim evicts hot file pages before cold
anonymous pages.  swappiness cannot correct it.  This series adds a
per-cgroup proactive-aging operation, memory.aging, with the
observability to drive it as a closed loop, so userspace can rebalance
a cgroup's generations before reclaim runs.

The problem
-----------

MGLRU places pages by access path, not hotness.  On the current tree
the entry rules in lru_gen_folio_seq() give:

  - faulted-in pages (anonymous pages; the fault path sets the active
    mark) land at the 2nd youngest generation (max_seq - 1);
  - file pages from read()/fadvise() land in the oldest two
    generations: non-workingset at min_seq, workingset at min_seq + 1.

Pictorially, for a full four-generation window::

       youngest                          oldest
       max_seq   max_seq-1   min_seq+1   min_seq
       -------------------------------------------
 anon    .          X           .           .     (faulted in)
 file    .          .           X           X     (read/fadvise)
                                            ^
                              reclaim scans from here -> evicts file first

Reclaim scans from min_seq, so file pages are evicted before the
anonymous pages two generations younger, a hot/cold inversion.

swappiness does not fix it: it only selects which type to scan, not
the ordering within a type, so reclaim can still evict hot pages.
swappiness=201 (SWAPPINESS_ANON_ONLY) just flips the inversion to the
other side: no value recovers a correct hot/cold order, because the
order comes from the access path, not access recency [1].  This is not
Android-specific; the same file over-reclaim is shown on servers and
worked around there by forcing an age iteration before reclaim.

Why userspace-driven
--------------------

The benefit is workload-dependent: file-cache-bound servers gain from
aging, anon-bound servers do not, so no kernel default is correct for
all.  The kernel also cannot know when to age: on Android the right
moment is the foreground-to-background transition, when the app's pages
are cold but their PTE accessed bits are still accurate from foreground
execution, a framework concept.  The kernel therefore provides the
mechanism; userspace runs the loop.

Design: observation and control
-------------------------------

The series gives userspace one control primitive and the observation to
use it.  All three pieces are per-cgroup and gated by CONFIG_LRU_GEN,
and together they form a closed loop:

  control   memory.aging          write nr_gens to advance max_seq without
                                  reclaim; the aging counterpart of
                                  memory.reclaim
  observe   nr_oldest_anon/file   pages in the oldest generation of each
                                  type: what the next reclaim can evict
            aging                 number of aging passes, paired with
                                  workingset_refault to spot over-aging

A policy reads nr_oldest_* to see whether anon is sheltered, ages with
memory.aging, then checks the aging counter and workingset_refault.
A typical flow ages to rebalance, then reclaims::

    echo 2 > /sys/fs/cgroup/foo/memory.aging
    echo "100M" > /sys/fs/cgroup/foo/memory.reclaim

Why cgroup v2
-------------

memory.reclaim already lives in cgroup v2, but it is broken when
anonymous pages are sheltered in the young generations: reclaim cannot
reach anon until aging advances it.  A useful memory.reclaim therefore
requires aging first.  Aging is not a new MGLRU detail leaked into
cgroup v2; it is the other half of the proactive-reclaim operation
cgroup v2 already exposes.  Putting it anywhere else (sysfs) cuts one
operation across two ABIs and two addressing models.

memory.aging is gated by CONFIG_LRU_GEN and exposes one primitive
(advancing the generation), not internal data structures; if MGLRU is
removed, the file goes with it.

Alternatives considered
-----------------------

1) sysfs (/sys/kernel/mm/lru_gen/) fights sysfs's one-value-per-file,
stateless model: the command is multi-argument and per-(memcg,node),
the read is parameterized per memcg, and css-id addressing is hostile
to cgroup paths.  It also splits aging from memory.reclaim.

2) in-kernel automatic aging is workload-dependent (memcached regresses)
and needs a trigger the kernel cannot know: the Android
foreground-to-background moment is a framework concept.  MGLRU had this
once: `should_run_aging' fired on a generation-balance heuristic::

    if (young * MIN_NR_GENS > total)
        return true;
    if (old * (MIN_NR_GENS + 2) < total)
        return true;


3) extending memory.reclaim to run the debugfs aging command
(`+ memcg_id node_id seq`) overloads a reclaim file with a non-reclaim
operation and pulls MGLRU's debugfs command syntax into a generic
cgroup v2 ABI. But Aging is reclaim's counterpart, not one of its modes.

Results
-------
N39 (Device-X1 8 GB/SM7750, Device-X2 12 GB/SM8750).  Production telemetry,
~300 users, 30K+ device-hours:
    keep-alive          +15.4% (8 GB) / +10.2% (12 GB)
    IME dismiss latency -47.2%
    launch/exit jank >50ms   eliminated
    one aging pass      342 us median, never on a UX thread

AN90 (a new shipping product, separate from N39):
    scroll jank >50 ms  -22%, severe jank -26%
    cold-launch stalls >= 3 s  eliminated
    workingset_refault (anon + file) and direct reclaim both down

Server (Intel i7-14700F, 20C/28T, 32 GB DDR5-5600, 1 TB NVMe,
Ubuntu 24.04; same root cause, aging triggered on the
reclaim path when anon in the oldest gen ratio < 10%):
    fio random-read     +31.8%
    ripgrep             +15.8%
    memcached           -1.2%   (anon-bound; expected no gain)

N39 and AN90 are different products.

Why RFC
-------

v1 [2] put the control in procfs/debugfs and was NAKed for the
cgroup-v2 venue.  This version keeps aging in cgroup v2, next to
memory.reclaim, and argues why above.  Discussed at LSF/MM 2026 [3].

[1] swappiness=0/201 hot/cold inversion, and why the straightforward
    fix is deferred:
    https://lore.kernel.org/linux-mm/7829b070df1b405dbc97dd6a028d8c8a@honor.com/
[2] v1: https://lore.kernel.org/linux-mm/20251128025315.3520689-1-wangzicheng@honor.com/
[3] LSF/MM 2026 slides:
    https://docs.google.com/presentation/d/1hUogz6InyLn13c8CjHuvEIzE4rT7saVRUV6xpWZoNfQ/

Changes since v1
----------------

- Dropped the debugfs -> procfs move; aging is now a cgroup v2 file.
- Added the AGING counter and nr_oldest_anon/file observability.
- Corrected the generation-placement description to the current tree
  (faults at the 2nd youngest, file in the oldest two).

Zicheng Wang (3):
  mm/lru_gen: add AGING counter and proactive aging helper
  mm: memcontrol: add memory.aging cgroup v2 file
  mm/lru_gen: expose oldest-generation page counts in memory.stat

 Documentation/admin-guide/cgroup-v2.rst       |  53 +++++++++++
 Documentation/admin-guide/mm/multigen_lru.rst |  13 +++
 include/linux/mmzone.h                        |   3 +
 mm/internal.h                                 |  19 +++++
 mm/memcontrol.c                               |  63 +++++++++++
 mm/vmscan.c                                   | 109 +++++++++++++++++++++
 mm/vmstat.c                                   |   3 +
 7 files changed, 263 insertions(+)

--
2.25.1



^ permalink raw reply	[flat|nested] 6+ messages in thread

* [RFC v2 1/3] mm/lru_gen: add AGING counter and proactive aging helper
  2026-07-14 12:15 [RFC v2 0/3] mm/mglru: proactive aging via memory.aging Zicheng Wang
@ 2026-07-14 12:15 ` Zicheng Wang
  2026-07-14 12:15 ` [RFC v2 2/3] mm: memcontrol: add memory.aging cgroup v2 file Zicheng Wang
                   ` (3 subsequent siblings)
  4 siblings, 0 replies; 6+ messages in thread
From: Zicheng Wang @ 2026-07-14 12:15 UTC (permalink / raw)
  To: akpm, yuanchu
  Cc: tj, hannes, mkoutny, corbet, kasong, qi.zheng, shakeel.butt,
	baohua, axelrasmussen, weixugc, david, ljs, liam, vbabka, rppt,
	surenb, mhocko, roman.gushchin, muchun.song, cgroups, linux-mm,
	linux-kernel, linux-doc, willy, denghaojie, baoquan.he,
	kaleshsingh, tjmercier, tao.wangtao, zhangji1, wangzhen5,
	Zicheng Wang

Add the two pieces the rest of the series builds on: an AGING node
stat counter (the observation an aging policy pairs with
workingset_refault to spot over-aging) and a reusable helper,
lru_gen_age_memcg(), that ages a memcg by advancing max_seq without
reclaim (the control primitive memory.aging wraps).

The counter counts every pass in inc_max_seq(), regardless of caller.
The helper mirrors lru_gen_seq_write()'s reclaim_state /
memalloc_noreclaim / mm_walk setup; swappiness comes from
get_swappiness() so anon is not rotated without swap, and a racing
lruvec is skipped (-EAGAIN) without blocking other nodes.

Gated by CONFIG_LRU_GEN.  No user-visible change yet.

Signed-off-by: Zicheng Wang <wangzicheng@honor.com>
---
 include/linux/mmzone.h |  3 ++
 mm/internal.h          |  4 +++
 mm/memcontrol.c        |  9 +++++
 mm/vmscan.c            | 80 ++++++++++++++++++++++++++++++++++++++++++
 mm/vmstat.c            |  3 ++
 5 files changed, 99 insertions(+)

diff --git a/include/linux/mmzone.h b/include/linux/mmzone.h
index 9adb2ad21da5..ab5137c272ca 100644
--- a/include/linux/mmzone.h
+++ b/include/linux/mmzone.h
@@ -323,6 +323,9 @@ enum node_stat_item {
 	PGSCAN_ANON,
 	PGSCAN_FILE,
 	PGREFILL,
+#ifdef CONFIG_LRU_GEN
+	AGING,
+#endif
 #ifdef CONFIG_HUGETLB_PAGE
 	NR_HUGETLB,
 #endif
diff --git a/mm/internal.h b/mm/internal.h
index 5a2ddcf68e0b..96add6cd4627 100644
--- a/mm/internal.h
+++ b/mm/internal.h
@@ -634,6 +634,10 @@ extern void reclaim_throttle(pg_data_t *pgdat, enum vmscan_throttle_state reason
 int user_proactive_reclaim(char *buf,
 			   struct mem_cgroup *memcg, pg_data_t *pgdat);
 
+#ifdef CONFIG_LRU_GEN
+int lru_gen_age_memcg(struct mem_cgroup *memcg, unsigned long nr_gens);
+#endif
+
 /*
  * in mm/rmap.c:
  */
diff --git a/mm/memcontrol.c b/mm/memcontrol.c
index d978e18b9b2d..e42f97e004b6 100644
--- a/mm/memcontrol.c
+++ b/mm/memcontrol.c
@@ -420,6 +420,9 @@ static const unsigned int memcg_node_stat_items[] = {
 	PGSCAN_ANON,
 	PGSCAN_FILE,
 	PGREFILL,
+#ifdef CONFIG_LRU_GEN
+	AGING,
+#endif
 #ifdef CONFIG_HUGETLB_PAGE
 	NR_HUGETLB,
 #endif
@@ -1606,6 +1609,9 @@ static const struct memory_stat memory_stats[] = {
 #ifdef CONFIG_NUMA_BALANCING
 	{ "pgpromote_success",		PGPROMOTE_SUCCESS	},
 #endif
+#ifdef CONFIG_LRU_GEN
+	{ "aging",		AGING	},
+#endif
 };
 
 /* The actual unit of the state item, not the same as the output unit */
@@ -1655,6 +1661,9 @@ static int memcg_page_state_output_unit(int item)
 	case PGSCAN_KHUGEPAGED:
 	case PGSCAN_PROACTIVE:
 	case PGREFILL:
+#ifdef CONFIG_LRU_GEN
+	case AGING:
+#endif
 #ifdef CONFIG_NUMA_BALANCING
 	case PGPROMOTE_SUCCESS:
 #endif
diff --git a/mm/vmscan.c b/mm/vmscan.c
index b3e555561417..805e29c499c8 100644
--- a/mm/vmscan.c
+++ b/mm/vmscan.c
@@ -3987,6 +3987,9 @@ static bool inc_max_seq(struct lruvec *lruvec, unsigned long seq, int swappiness
 	WRITE_ONCE(lrugen->timestamps[next], jiffies);
 	/* make sure preceding modifications appear */
 	smp_store_release(&lrugen->max_seq, lrugen->max_seq + 1);
+
+	/* Count every aging pass for over-aging diagnosis. */
+	mod_lruvec_state(lruvec, AGING, 1);
 unlock:
 	lruvec_unlock_irq(lruvec);
 
@@ -5855,6 +5858,83 @@ static int __init init_lru_gen(void)
 };
 late_initcall(init_lru_gen);
 
+/**
+ * lru_gen_age_memcg - Proactively age a memcg by N generations
+ * @memcg: target memcg
+ * @nr_gens: number of generations to age (1..MAX_NR_GENS)
+ *
+ * Advances max_seq without page reclaim on all nodes of @memcg, using
+ * the same lock/walk protocol as lru_gen_seq_write().  Swappiness comes
+ * from get_swappiness() so anon rotation is skipped without swap.
+ * to the next node.
+ *
+ * Return: 0 on success, -EINVAL on bad input, -EAGAIN if a lruvec did
+ * not advance, -ENOMEM on mm_walk failure, -EINTR on signal.
+ */
+int lru_gen_age_memcg(struct mem_cgroup *memcg, unsigned long nr_gens)
+{
+	struct scan_control sc = {
+		.may_writepage = true,
+		.may_unmap = true,
+		.may_swap = true,
+		.reclaim_idx = MAX_NR_ZONES - 1,
+		.gfp_mask = GFP_KERNEL,
+		.proactive = true,
+	};
+	int nid, swappiness;
+	unsigned long i;
+	int ret = 0;
+	unsigned int flags;
+	struct blk_plug plug;
+	struct lruvec *lruvec;
+
+	if (!memcg)
+		return -EINVAL;
+
+	if (nr_gens == 0 || nr_gens > MAX_NR_GENS)
+		return -EINVAL;
+
+	set_task_reclaim_state(current, &sc.reclaim_state);
+	flags = memalloc_noreclaim_save();
+	blk_start_plug(&plug);
+	if (!set_mm_walk(NULL, true)) {
+		ret = -ENOMEM;
+		goto done;
+	}
+
+	for_each_node_state(nid, N_MEMORY) {
+		if (signal_pending(current)) {
+			ret = -EINTR;
+			goto done;
+		}
+
+		lruvec = get_lruvec(memcg, nid);
+		if (!lruvec)
+			continue;
+
+		swappiness = get_swappiness(lruvec, &sc);
+
+		for (i = 0; i < nr_gens; i++) {
+			DEFINE_MAX_SEQ(lruvec);
+
+			if (!try_to_inc_max_seq(lruvec, max_seq,
+						swappiness, false)) {
+				ret = -EAGAIN;
+				break;
+			}
+		}
+
+		cond_resched();
+	}
+
+done:
+	clear_mm_walk();
+	blk_finish_plug(&plug);
+	memalloc_noreclaim_restore(flags);
+	set_task_reclaim_state(current, NULL);
+	return ret;
+}
+
 #else /* !CONFIG_LRU_GEN */
 
 static void lru_gen_age_node(struct pglist_data *pgdat, struct scan_control *sc)
diff --git a/mm/vmstat.c b/mm/vmstat.c
index f534972f517d..78ea093428c7 100644
--- a/mm/vmstat.c
+++ b/mm/vmstat.c
@@ -1290,6 +1290,9 @@ const char * const vmstat_text[] = {
 	[I(PGSCAN_ANON)]			= "pgscan_anon",
 	[I(PGSCAN_FILE)]			= "pgscan_file",
 	[I(PGREFILL)]				= "pgrefill",
+#ifdef CONFIG_LRU_GEN
+	[I(AGING)]			= "aging",
+#endif
 #ifdef CONFIG_HUGETLB_PAGE
 	[I(NR_HUGETLB)]				= "nr_hugetlb",
 #endif
-- 
2.25.1



^ permalink raw reply related	[flat|nested] 6+ messages in thread

* [RFC v2 2/3] mm: memcontrol: add memory.aging cgroup v2 file
  2026-07-14 12:15 [RFC v2 0/3] mm/mglru: proactive aging via memory.aging Zicheng Wang
  2026-07-14 12:15 ` [RFC v2 1/3] mm/lru_gen: add AGING counter and proactive aging helper Zicheng Wang
@ 2026-07-14 12:15 ` Zicheng Wang
  2026-07-14 12:15 ` [RFC v2 3/3] mm/lru_gen: expose oldest-generation page counts in memory.stat Zicheng Wang
                   ` (2 subsequent siblings)
  4 siblings, 0 replies; 6+ messages in thread
From: Zicheng Wang @ 2026-07-14 12:15 UTC (permalink / raw)
  To: akpm, yuanchu
  Cc: tj, hannes, mkoutny, corbet, kasong, qi.zheng, shakeel.butt,
	baohua, axelrasmussen, weixugc, david, ljs, liam, vbabka, rppt,
	surenb, mhocko, roman.gushchin, muchun.song, cgroups, linux-mm,
	linux-kernel, linux-doc, willy, denghaojie, baoquan.he,
	kaleshsingh, tjmercier, tao.wangtao, zhangji1, wangzhen5,
	Zicheng Wang

Add a write-only memory.aging that triggers MGLRU aging (max_seq
advancement) without reclaim - the aging counterpart of memory.reclaim.
Userspace ages to rebalance the anon/file distribution across
generations, then reclaims.  This is the control half of the loop;
the observability it needs lands in the next patch.

Useful for workloads such as the Android app lifecycle, where anon
concentrates in the 2nd-youngest generation while file fills the
oldest two and gets over-reclaimed.

  echo 2 > /sys/fs/cgroup/foo/memory.aging
  echo "100M" > /sys/fs/cgroup/foo/memory.reclaim

Gated by CONFIG_LRU_GEN.  Concurrent writers race on the lruvec lock;
a caller that did not advance the window gets -EAGAIN.

Signed-off-by: Zicheng Wang <wangzicheng@honor.com>
---
 Documentation/admin-guide/cgroup-v2.rst       | 35 +++++++++++++++++++
 Documentation/admin-guide/mm/multigen_lru.rst | 13 +++++++
 mm/memcontrol.c                               | 30 ++++++++++++++++
 3 files changed, 78 insertions(+)

diff --git a/Documentation/admin-guide/cgroup-v2.rst b/Documentation/admin-guide/cgroup-v2.rst
index 6efd0095ed99..f42ac6dddbf6 100644
--- a/Documentation/admin-guide/cgroup-v2.rst
+++ b/Documentation/admin-guide/cgroup-v2.rst
@@ -1437,6 +1437,41 @@ The following nested keys are defined.
 	The valid range for swappiness is [0-200, max], setting
 	swappiness=max exclusively reclaims anonymous memory.
 
+  memory.aging
+	A write-only file which exists for all cgroups when
+	CONFIG_LRU_GEN is enabled.
+
+	This interface triggers MGLRU aging (generation advancement)
+	without page reclaim. It is useful for proactively rebalancing
+	the distribution of pages across generations before invoking
+	memory.reclaim, particularly for workloads (such as the Android
+	application lifecycle) where anonymous pages concentrate in
+	the youngest generations while file pages fill up the oldest
+	generation and get over-reclaimed.
+
+	The single argument is the number of generations to age, in
+	the range [1, MAX_NR_GENS]. MAX_NR_GENS is currently 4.
+
+	Example::
+
+	  echo 2 > memory.aging
+
+	Each aging pass advances both anonymous and file generations
+	simultaneously, matching MGLRU's max_seq semantics. There is
+	no way to age only one type - this is a property of MGLRU,
+	not of this interface.
+
+	Proactive aging should be followed by memory.reclaim to
+	actually free pages::
+
+	  echo 2 > /sys/fs/cgroup/foo/memory.aging
+	  echo "100M" > /sys/fs/cgroup/foo/memory.reclaim
+
+	Returns -EAGAIN if this call did not advance the generation
+	window (typically because a concurrent caller on the same
+	cgroup got there first). Returns -EINVAL if the argument is
+	missing, zero, or out of range.
+
   memory.peak
 	A read-write single value file which exists on non-root cgroups.
 
diff --git a/Documentation/admin-guide/mm/multigen_lru.rst b/Documentation/admin-guide/mm/multigen_lru.rst
index 9cb54b4ff5d9..fd8b4286014e 100644
--- a/Documentation/admin-guide/mm/multigen_lru.rst
+++ b/Documentation/admin-guide/mm/multigen_lru.rst
@@ -161,3 +161,16 @@ cold pages because of the overestimation, it retries on the next
 server according to the ranking result obtained from the working set
 estimation step. This less forceful approach limits the impacts on the
 existing jobs.
+
+Proactive aging
+---------------
+For workloads where it is useful to rebalance the distribution of
+anonymous and file pages across generations before reclaiming (for
+example, the Android application lifecycle, where an application
+that has just been backgrounded has most of its anonymous pages
+clustered in the youngest generations while file pages dominate
+the oldest), the ``memory.aging`` cgroup v2 file can be used to
+advance the generation counter without eviction. The file returns
+-EAGAIN if no advance happened, so callers can correlate with the
+``aging`` counter to verify how many passes actually took
+effect. See Documentation/admin-guide/cgroup-v2.rst.
diff --git a/mm/memcontrol.c b/mm/memcontrol.c
index e42f97e004b6..c6a969b56878 100644
--- a/mm/memcontrol.c
+++ b/mm/memcontrol.c
@@ -4896,6 +4896,29 @@ static ssize_t memory_reclaim(struct kernfs_open_file *of, char *buf,
 	return nbytes;
 }
 
+#ifdef CONFIG_LRU_GEN
+static ssize_t memory_aging(struct kernfs_open_file *of, char *buf,
+			    size_t nbytes, loff_t off)
+{
+	struct mem_cgroup *memcg = mem_cgroup_from_css(of_css(of));
+	unsigned long nr_gens;
+	int ret;
+
+	ret = kstrtoul(buf, 0, &nr_gens);
+	if (ret)
+		return ret;
+
+	if (nr_gens == 0 || nr_gens > MAX_NR_GENS)
+		return -EINVAL;
+
+	ret = lru_gen_age_memcg(memcg, nr_gens);
+	if (ret)
+		return ret;
+
+	return nbytes;
+}
+#endif
+
 static struct cftype memory_files[] = {
 	{
 		.name = "current",
@@ -4967,6 +4990,13 @@ static struct cftype memory_files[] = {
 		.flags = CFTYPE_NS_DELEGATABLE,
 		.write = memory_reclaim,
 	},
+#ifdef CONFIG_LRU_GEN
+	{
+		.name = "aging",
+		.flags = CFTYPE_NS_DELEGATABLE,
+		.write = memory_aging,
+	},
+#endif
 	{ }	/* terminate */
 };
 
-- 
2.25.1



^ permalink raw reply related	[flat|nested] 6+ messages in thread

* [RFC v2 3/3] mm/lru_gen: expose oldest-generation page counts in memory.stat
  2026-07-14 12:15 [RFC v2 0/3] mm/mglru: proactive aging via memory.aging Zicheng Wang
  2026-07-14 12:15 ` [RFC v2 1/3] mm/lru_gen: add AGING counter and proactive aging helper Zicheng Wang
  2026-07-14 12:15 ` [RFC v2 2/3] mm: memcontrol: add memory.aging cgroup v2 file Zicheng Wang
@ 2026-07-14 12:15 ` Zicheng Wang
  2026-07-15 14:00 ` [RFC v2 0/3] mm/mglru: proactive aging via memory.aging Johannes Weiner
  2026-07-15 17:55 ` T.J. Mercier
  4 siblings, 0 replies; 6+ messages in thread
From: Zicheng Wang @ 2026-07-14 12:15 UTC (permalink / raw)
  To: akpm, yuanchu
  Cc: tj, hannes, mkoutny, corbet, kasong, qi.zheng, shakeel.butt,
	baohua, axelrasmussen, weixugc, david, ljs, liam, vbabka, rppt,
	surenb, mhocko, roman.gushchin, muchun.song, cgroups, linux-mm,
	linux-kernel, linux-doc, willy, denghaojie, baoquan.he,
	kaleshsingh, tjmercier, tao.wangtao, zhangji1, wangzhen5,
	Zicheng Wang

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@honor.com>
---
 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



^ permalink raw reply related	[flat|nested] 6+ messages in thread

* Re: [RFC v2 0/3] mm/mglru: proactive aging via memory.aging
  2026-07-14 12:15 [RFC v2 0/3] mm/mglru: proactive aging via memory.aging Zicheng Wang
                   ` (2 preceding siblings ...)
  2026-07-14 12:15 ` [RFC v2 3/3] mm/lru_gen: expose oldest-generation page counts in memory.stat Zicheng Wang
@ 2026-07-15 14:00 ` Johannes Weiner
  2026-07-15 17:55 ` T.J. Mercier
  4 siblings, 0 replies; 6+ messages in thread
From: Johannes Weiner @ 2026-07-15 14:00 UTC (permalink / raw)
  To: Zicheng Wang
  Cc: akpm, yuanchu, tj, mkoutny, corbet, kasong, qi.zheng,
	shakeel.butt, baohua, axelrasmussen, weixugc, david, ljs, liam,
	vbabka, rppt, surenb, mhocko, roman.gushchin, muchun.song,
	cgroups, linux-mm, linux-kernel, linux-doc, willy, denghaojie,
	baoquan.he, kaleshsingh, tjmercier, tao.wangtao, zhangji1,
	wangzhen5

On Tue, Jul 14, 2026 at 08:15:26PM +0800, Zicheng Wang wrote:
> MGLRU inverts the reclaim order when anonymous memory is faulted in
> bulk: anonymous pages sit in the young generations while file pages
> sit in the oldest two, so reclaim evicts hot file pages before cold
> anonymous pages.

An aging inversion in the reclaim algorithm seems like an exceedingly
poor justification for a userspace interface to work around them.


^ permalink raw reply	[flat|nested] 6+ messages in thread

* Re: [RFC v2 0/3] mm/mglru: proactive aging via memory.aging
  2026-07-14 12:15 [RFC v2 0/3] mm/mglru: proactive aging via memory.aging Zicheng Wang
                   ` (3 preceding siblings ...)
  2026-07-15 14:00 ` [RFC v2 0/3] mm/mglru: proactive aging via memory.aging Johannes Weiner
@ 2026-07-15 17:55 ` T.J. Mercier
  4 siblings, 0 replies; 6+ messages in thread
From: T.J. Mercier @ 2026-07-15 17:55 UTC (permalink / raw)
  To: Zicheng Wang
  Cc: akpm, yuanchu, tj, hannes, mkoutny, corbet, kasong, qi.zheng,
	shakeel.butt, baohua, axelrasmussen, weixugc, david, ljs, liam,
	vbabka, rppt, surenb, mhocko, roman.gushchin, muchun.song,
	cgroups, linux-mm, linux-kernel, linux-doc, willy, denghaojie,
	baoquan.he, kaleshsingh, tao.wangtao, zhangji1, wangzhen5

On Tue, Jul 14, 2026 at 5:15 AM Zicheng Wang <wangzicheng@honor.com> wrote:
> The benefit is workload-dependent: file-cache-bound servers gain from
> aging, anon-bound servers do not, so no kernel default is correct for
> all.  The kernel also cannot know when to age: on Android the right
> moment is the foreground-to-background transition, when the app's pages
> are cold but their PTE accessed bits are still accurate from foreground
> execution, a framework concept.

When an app transitions and becomes cached, we attempt to reclaim its
entire workingset. We basically cat memory.current > memory.reclaim
and freeze the cgroup.
https://cs.android.com/android/platform/superproject/+/android-latest-release:system/core/libprocessgroup/task_profiles.cpp;drc=65bd7ab941a709bf049871406981022b988e1721;l=706

File / anon balance vs hotness and generational placement doesn't
matter in that scenario because we want to get rid of all of it. So I
don't really understand how you'd want to use an aging knob.

^ permalink raw reply	[flat|nested] 6+ messages in thread

end of thread, other threads:[~2026-07-15 17:56 UTC | newest]

Thread overview: 6+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-07-14 12:15 [RFC v2 0/3] mm/mglru: proactive aging via memory.aging Zicheng Wang
2026-07-14 12:15 ` [RFC v2 1/3] mm/lru_gen: add AGING counter and proactive aging helper Zicheng Wang
2026-07-14 12:15 ` [RFC v2 2/3] mm: memcontrol: add memory.aging cgroup v2 file Zicheng Wang
2026-07-14 12:15 ` [RFC v2 3/3] mm/lru_gen: expose oldest-generation page counts in memory.stat Zicheng Wang
2026-07-15 14:00 ` [RFC v2 0/3] mm/mglru: proactive aging via memory.aging Johannes Weiner
2026-07-15 17:55 ` T.J. Mercier

This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.