* [PATCH v2 0/3] mm/mglru: add tracepoints for scan and aging paths
@ 2026-09-11 10:29 Ridong Chen
2026-09-11 10:29 ` [PATCH v2 1/3] mm/mglru: factor out lru_gen_seq_nr_pages() Ridong Chen
` (2 more replies)
0 siblings, 3 replies; 4+ messages in thread
From: Ridong Chen @ 2026-09-11 10:29 UTC (permalink / raw)
To: Steven Rostedt, Masami Hiramatsu, Andrew Morton, Johannes Weiner
Cc: Mathieu Desnoyers, Kairui Song, Qi Zheng, Shakeel Butt,
Barry Song, Axel Rasmussen, Yuanchu Xie, Wei Xu, Baoquan He,
Baolin Wang, David Hildenbrand, Michal Hocko, Lorenzo Stoakes,
linux-kernel, linux-trace-kernel,
open list:MEMORY MANAGEMENT - MGLRU (MULTI-GEN LRU), Ridong Chen,
Ridong Chen
From: Ridong Chen <chenridong@xiaomi.com>
Background
==========
MGLRU currently has no tracepoints of its own. The scan and evict paths
reuse the classic-LRU tracepoints (trace_mm_vmscan_lru_isolate() and
trace_mm_vmscan_lru_shrink_inactive()), which predate MGLRU and carry no
generation, sequence, memcg or swappiness context. A trace of a running
system therefore cannot tell which memcg a given scan belongs to, how far
reclaim has progressed through the generations, or when a new generation
is created - so how MGLRU actually operates is effectively invisible.
Implementation
==============
Patch 1 is a cleanup that factors the per-generation page-count
summation into a helper, lru_gen_seq_nr_pages(), reused by patch 3.
Patch 2 adds mm_mglru_scan_folios on the scan path, and patch 3 adds
mm_mglru_inc_max_seq on the aging path. Both carry the memcg id and the
generation window (min_seq/max_seq), live at MGLRU-specific layers with
no classic-LRU counterpart, and are guarded so the hot paths stay
zero-cost when disabled.
Effect
======
Paired, the two tracepoints make the full aging-to-eviction window
observable per memcg: aging advances max_seq (the leading edge), scanning
consumes the oldest generations, and the min_seq/max_seq pair on each
event shows how the generation window moves over time.
A sample trace, with the classic-LRU tracepoints left enabled to show
how they interleave:
mm_vmscan_lru_isolate: classzone=4 order=0 nr_requested=36 nr_scanned=31 nr_skipped=0 nr_taken=29 lru=inactive_file
mm_mglru_scan_folios: memcg_id=73 classzone=4 order=0 nr_requested=36 nr_scanned=31 nr_sorted=2 nr_skipped=0 nr_taken=29 lru=inactive_file max_seq=3 tier=3 min_seq=0
mm_vmscan_lru_shrink_inactive: nid=0 nr_scanned=31 nr_reclaimed=29 nr_dirty=0 nr_writeback=0 nr_congested=0 nr_immediate=0 nr_activate_anon=0 nr_activate_file=0 nr_ref_keep=0 nr_unmap_fail=0 priority=5 flags=RECLAIM_WB_FILE|RECLAIM_WB_ASYNC
mm_mglru_inc_max_seq: memcg_id=73 max_seq=4 anon_min_seq=1 file_min_seq=1 nr_anon={0x0,0x0,0x1,0x0} nr_file={0x0,0x427,0x6,0x4a}
mm_vmscan_lru_isolate: classzone=4 order=0 nr_requested=5 nr_scanned=5 nr_skipped=0 nr_taken=1 lru=inactive_file
mm_mglru_scan_folios: memcg_id=73 classzone=4 order=0 nr_requested=5 nr_scanned=5 nr_sorted=4 nr_skipped=0 nr_taken=1 lru=inactive_file max_seq=4 tier=3 min_seq=1
mm_vmscan_lru_shrink_inactive: nid=0 nr_scanned=5 nr_reclaimed=1 nr_dirty=0 nr_writeback=0 nr_congested=0 nr_immediate=0 nr_activate_anon=0 nr_activate_file=0 nr_ref_keep=0 nr_unmap_fail=0 priority=5 flags=RECLAIM_WB_FILE|RECLAIM_WB_ASYNC
The mm_mglru_scan_folios lines carry the memcg id and the generation
window (max_seq/min_seq) that the bare mm_vmscan_lru_isolate lines above
them cannot - those cannot even say which memcg they came from. The
mm_mglru_inc_max_seq line then shows a new generation being created
(max_seq 3 -> 4) with the per-generation page counts for both types.
---
v2: address Sashiko AI review [1] - pass struct lruvec * into the tracepoints
and do the memcg lookup inside TP_fast_assign.
[1] https://sashiko.dev/#/patchset/20260911072848.2346073-1-ridong.chen@linux.dev?part=2
Ridong Chen (3):
mm/mglru: factor out lru_gen_seq_nr_pages()
mm/mglru: add tracepoint for scan_folios()
mm/mglru: add tracepoint for inc_max_seq()
include/trace/events/vmscan.h | 101 ++++++++++++++++++++++++++++++++++
mm/vmscan.c | 53 +++++++++++++++---
2 files changed, 146 insertions(+), 8 deletions(-)
--
2.34.1
^ permalink raw reply [flat|nested] 4+ messages in thread
* [PATCH v2 1/3] mm/mglru: factor out lru_gen_seq_nr_pages()
2026-09-11 10:29 [PATCH v2 0/3] mm/mglru: add tracepoints for scan and aging paths Ridong Chen
@ 2026-09-11 10:29 ` Ridong Chen
2026-09-11 10:29 ` [PATCH v2 2/3] mm/mglru: add tracepoint for scan_folios() Ridong Chen
2026-09-11 10:29 ` [PATCH v2 3/3] mm/mglru: add tracepoint for inc_max_seq() Ridong Chen
2 siblings, 0 replies; 4+ messages in thread
From: Ridong Chen @ 2026-09-11 10:29 UTC (permalink / raw)
To: Steven Rostedt, Masami Hiramatsu, Andrew Morton, Johannes Weiner
Cc: Mathieu Desnoyers, Kairui Song, Qi Zheng, Shakeel Butt,
Barry Song, Axel Rasmussen, Yuanchu Xie, Wei Xu, Baoquan He,
Baolin Wang, David Hildenbrand, Michal Hocko, Lorenzo Stoakes,
linux-kernel, linux-trace-kernel,
open list:MEMORY MANAGEMENT - MGLRU (MULTI-GEN LRU), Ridong Chen,
Ridong Chen
From: Ridong Chen <chenridong@xiaomi.com>
Both lruvec_evictable_size() and the debugfs lru_gen_seq_show() compute
the number of pages in a generation the same way: sum lrugen->nr_pages
over all zones for a given (gen, type) and clamp each term to >= 0.
Factor that out into lru_gen_seq_nr_pages() so the open-coded zone loop
lives in one place. No functional change.
A follow-up patch adds a tracepoint that needs the same per-generation
page count, and will reuse this helper instead of open-coding it again.
Assisted-by: Claude:claude-opus-4-8
Signed-off-by: Ridong Chen <chenridong@xiaomi.com>
---
mm/vmscan.c | 24 ++++++++++++++++--------
1 file changed, 16 insertions(+), 8 deletions(-)
diff --git a/mm/vmscan.c b/mm/vmscan.c
index 40d3f1b48a74..2554a6513aa8 100644
--- a/mm/vmscan.c
+++ b/mm/vmscan.c
@@ -2813,6 +2813,18 @@ static int get_nr_gens(struct lruvec *lruvec, int type)
return lruvec->lrugen.max_seq - lruvec->lrugen.min_seq[type] + 1;
}
+/* the number of pages in a generation, summed over zones and clamped to >= 0 */
+static unsigned long lru_gen_seq_nr_pages(struct lru_gen_folio *lrugen, int gen, int type)
+{
+ int zone;
+ unsigned long size = 0;
+
+ for (zone = 0; zone < MAX_NR_ZONES; zone++)
+ size += max(READ_ONCE(lrugen->nr_pages[gen][type][zone]), 0L);
+
+ return size;
+}
+
static bool __maybe_unused seq_is_valid(struct lruvec *lruvec)
{
int type;
@@ -4239,7 +4251,7 @@ static void set_initial_priority(struct pglist_data *pgdat, struct scan_control
static unsigned long lruvec_evictable_size(struct lruvec *lruvec, int swappiness)
{
- int gen, type, zone;
+ int gen, type;
unsigned long seq, total = 0;
struct lru_gen_folio *lrugen = &lruvec->lrugen;
DEFINE_MAX_SEQ(lruvec);
@@ -4248,8 +4260,7 @@ static unsigned long lruvec_evictable_size(struct lruvec *lruvec, int swappiness
for_each_evictable_type(type, swappiness) {
for (seq = min_seq[type]; seq <= max_seq; seq++) {
gen = lru_gen_from_seq(seq);
- for (zone = 0; zone < MAX_NR_ZONES; zone++)
- total += max(READ_ONCE(lrugen->nr_pages[gen][type][zone]), 0L);
+ total += lru_gen_seq_nr_pages(lrugen, gen, type);
}
}
@@ -5738,19 +5749,16 @@ static int lru_gen_seq_show(struct seq_file *m, void *v)
seq = 0;
for (; seq <= max_seq; seq++) {
- int type, zone;
+ int type;
int gen = lru_gen_from_seq(seq);
unsigned long birth = READ_ONCE(lruvec->lrugen.timestamps[gen]);
seq_printf(m, " %10lu %10u", seq, jiffies_to_msecs(jiffies - birth));
for (type = 0; type < ANON_AND_FILE; type++) {
- unsigned long size = 0;
+ unsigned long size = lru_gen_seq_nr_pages(lrugen, gen, type);
char mark = full && seq < min_seq[type] ? 'x' : ' ';
- for (zone = 0; zone < MAX_NR_ZONES; zone++)
- size += max(READ_ONCE(lrugen->nr_pages[gen][type][zone]), 0L);
-
seq_printf(m, " %10lu%c", size, mark);
}
--
2.34.1
^ permalink raw reply related [flat|nested] 4+ messages in thread
* [PATCH v2 2/3] mm/mglru: add tracepoint for scan_folios()
2026-09-11 10:29 [PATCH v2 0/3] mm/mglru: add tracepoints for scan and aging paths Ridong Chen
2026-09-11 10:29 ` [PATCH v2 1/3] mm/mglru: factor out lru_gen_seq_nr_pages() Ridong Chen
@ 2026-09-11 10:29 ` Ridong Chen
2026-09-11 10:29 ` [PATCH v2 3/3] mm/mglru: add tracepoint for inc_max_seq() Ridong Chen
2 siblings, 0 replies; 4+ messages in thread
From: Ridong Chen @ 2026-09-11 10:29 UTC (permalink / raw)
To: Steven Rostedt, Masami Hiramatsu, Andrew Morton, Johannes Weiner
Cc: Mathieu Desnoyers, Kairui Song, Qi Zheng, Shakeel Butt,
Barry Song, Axel Rasmussen, Yuanchu Xie, Wei Xu, Baoquan He,
Baolin Wang, David Hildenbrand, Michal Hocko, Lorenzo Stoakes,
linux-kernel, linux-trace-kernel,
open list:MEMORY MANAGEMENT - MGLRU (MULTI-GEN LRU), Ridong Chen,
Ridong Chen
From: Ridong Chen <chenridong@xiaomi.com>
MGLRU's scan_folios() emits the classic-LRU tracepoint
trace_mm_vmscan_lru_isolate(), which predates MGLRU. It reports the
scan/isolate counts and the LRU type, but carries no generation or
memcg context, so a trace of an MGLRU run cannot tell which memcg a
given scan belongs to, nor how far reclaim has progressed through the
generations.
Add mm_mglru_scan_folios next to it, reporting the same counters plus
nr_sorted (folios moved to a younger generation by sort_folio()) and
the MGLRU context the classic tracepoint lacks: the memcg id, and the
max_seq, tier and min_seq of the type being scanned.
scan_folios() is the MGLRU-specific layer where folios are actually
scanned, so it has no classic-LRU counterpart. That the classic
trace_mm_vmscan_lru_isolate() has lived here stably shows this is a
long-lived place to hook, and the new tracepoint can be enabled on its
own to observe the MGLRU-specific information. The existing tracepoint
is left unchanged.
Assisted-by: Claude:claude-opus-4-8
Signed-off-by: Ridong Chen <chenridong@xiaomi.com>
---
include/trace/events/vmscan.h | 63 +++++++++++++++++++++++++++++++++++
mm/vmscan.c | 6 ++++
2 files changed, 69 insertions(+)
diff --git a/include/trace/events/vmscan.h b/include/trace/events/vmscan.h
index 8a872990b4be..5defa8f6719c 100644
--- a/include/trace/events/vmscan.h
+++ b/include/trace/events/vmscan.h
@@ -392,6 +392,69 @@ TRACE_EVENT(mm_vmscan_lru_isolate,
__print_symbolic(__entry->lru, LRU_NAMES))
);
+TRACE_EVENT(mm_mglru_scan_folios,
+
+ TP_PROTO(struct lruvec *lruvec,
+ int highest_zoneidx,
+ int order,
+ unsigned long nr_requested,
+ unsigned long nr_scanned,
+ unsigned long nr_sorted,
+ unsigned long nr_skipped,
+ unsigned long nr_taken,
+ int lru,
+ unsigned long max_seq,
+ int tier,
+ unsigned long min_seq),
+
+ TP_ARGS(lruvec, highest_zoneidx, order, nr_requested, nr_scanned,
+ nr_sorted, nr_skipped, nr_taken, lru, max_seq, tier, min_seq),
+
+ TP_STRUCT__entry(
+ __field(u64, memcg_id)
+ __field(int, highest_zoneidx)
+ __field(int, order)
+ __field(unsigned long, nr_requested)
+ __field(unsigned long, nr_scanned)
+ __field(unsigned long, nr_sorted)
+ __field(unsigned long, nr_skipped)
+ __field(unsigned long, nr_taken)
+ __field(int, lru)
+ __field(unsigned long, max_seq)
+ __field(int, tier)
+ __field(unsigned long, min_seq)
+ ),
+
+ TP_fast_assign(
+ __entry->memcg_id = mem_cgroup_id(lruvec_memcg(lruvec));
+ __entry->highest_zoneidx = highest_zoneidx;
+ __entry->order = order;
+ __entry->nr_requested = nr_requested;
+ __entry->nr_scanned = nr_scanned;
+ __entry->nr_sorted = nr_sorted;
+ __entry->nr_skipped = nr_skipped;
+ __entry->nr_taken = nr_taken;
+ __entry->lru = lru;
+ __entry->max_seq = max_seq;
+ __entry->tier = tier;
+ __entry->min_seq = min_seq;
+ ),
+
+ TP_printk("memcg_id=%llu classzone=%d order=%d nr_requested=%lu nr_scanned=%lu nr_sorted=%lu nr_skipped=%lu nr_taken=%lu lru=%s max_seq=%lu tier=%d min_seq=%lu",
+ __entry->memcg_id,
+ __entry->highest_zoneidx,
+ __entry->order,
+ __entry->nr_requested,
+ __entry->nr_scanned,
+ __entry->nr_sorted,
+ __entry->nr_skipped,
+ __entry->nr_taken,
+ __print_symbolic(__entry->lru, LRU_NAMES),
+ __entry->max_seq,
+ __entry->tier,
+ __entry->min_seq)
+);
+
TRACE_EVENT(mm_vmscan_write_folio,
TP_PROTO(struct folio *folio),
diff --git a/mm/vmscan.c b/mm/vmscan.c
index 2554a6513aa8..67f59aa73fb9 100644
--- a/mm/vmscan.c
+++ b/mm/vmscan.c
@@ -4876,6 +4876,12 @@ static int scan_folios(unsigned long nr_to_scan, struct lruvec *lruvec,
trace_mm_vmscan_lru_isolate(sc->reclaim_idx, sc->order, nr_to_scan,
scanned, skipped, isolated,
type ? LRU_INACTIVE_FILE : LRU_INACTIVE_ANON);
+ trace_mm_mglru_scan_folios(lruvec,
+ sc->reclaim_idx, sc->order, nr_to_scan,
+ scanned, sorted, skipped, isolated,
+ type ? LRU_INACTIVE_FILE : LRU_INACTIVE_ANON,
+ lrugen->max_seq, tier,
+ lrugen->min_seq[type]);
*isolatedp = isolated;
return scanned;
--
2.34.1
^ permalink raw reply related [flat|nested] 4+ messages in thread
* [PATCH v2 3/3] mm/mglru: add tracepoint for inc_max_seq()
2026-09-11 10:29 [PATCH v2 0/3] mm/mglru: add tracepoints for scan and aging paths Ridong Chen
2026-09-11 10:29 ` [PATCH v2 1/3] mm/mglru: factor out lru_gen_seq_nr_pages() Ridong Chen
2026-09-11 10:29 ` [PATCH v2 2/3] mm/mglru: add tracepoint for scan_folios() Ridong Chen
@ 2026-09-11 10:29 ` Ridong Chen
2 siblings, 0 replies; 4+ messages in thread
From: Ridong Chen @ 2026-09-11 10:29 UTC (permalink / raw)
To: Steven Rostedt, Masami Hiramatsu, Andrew Morton, Johannes Weiner
Cc: Mathieu Desnoyers, Kairui Song, Qi Zheng, Shakeel Butt,
Barry Song, Axel Rasmussen, Yuanchu Xie, Wei Xu, Baoquan He,
Baolin Wang, David Hildenbrand, Michal Hocko, Lorenzo Stoakes,
linux-kernel, linux-trace-kernel,
open list:MEMORY MANAGEMENT - MGLRU (MULTI-GEN LRU), Ridong Chen,
Ridong Chen
From: Ridong Chen <chenridong@xiaomi.com>
Aging in MGLRU advances max_seq via inc_max_seq(), creating a new
youngest generation. There is currently no tracepoint on this path, so
the moment a new generation is created, and how the min_seq of each type
trails behind it, cannot be observed as it happens.
Add mm_mglru_inc_max_seq, emitted right after max_seq is bumped, with
the memcg id, the new max_seq, the anon and file min_seq, and the number
of pages in each generation for both types, summed over zones the same
way the debugfs lru_gen file reports them. The nr_anon/nr_file arrays are
indexed by generation slot, so the emitted max_seq/min_seq say which slot
holds which seq. They are printed with __print_array(), so the output is
hex and its length follows MAX_NR_GENS automatically. Paired with the
mm_mglru_scan_folios tracepoint it makes the full aging-to-eviction
window observable per memcg.
Assisted-by: Claude:claude-opus-4-8
Signed-off-by: Ridong Chen <chenridong@xiaomi.com>
---
include/trace/events/vmscan.h | 38 +++++++++++++++++++++++++++++++++++
mm/vmscan.c | 23 +++++++++++++++++++++
2 files changed, 61 insertions(+)
diff --git a/include/trace/events/vmscan.h b/include/trace/events/vmscan.h
index 5defa8f6719c..a20c6b2e062f 100644
--- a/include/trace/events/vmscan.h
+++ b/include/trace/events/vmscan.h
@@ -455,6 +455,44 @@ TRACE_EVENT(mm_mglru_scan_folios,
__entry->min_seq)
);
+TRACE_EVENT(mm_mglru_inc_max_seq,
+
+ TP_PROTO(struct lruvec *lruvec,
+ unsigned long max_seq,
+ unsigned long anon_min_seq,
+ unsigned long file_min_seq,
+ unsigned long *nr_anon,
+ unsigned long *nr_file),
+
+ TP_ARGS(lruvec, max_seq, anon_min_seq, file_min_seq, nr_anon, nr_file),
+
+ TP_STRUCT__entry(
+ __field(u64, memcg_id)
+ __field(unsigned long, max_seq)
+ __field(unsigned long, anon_min_seq)
+ __field(unsigned long, file_min_seq)
+ __array(unsigned long, nr_anon, MAX_NR_GENS)
+ __array(unsigned long, nr_file, MAX_NR_GENS)
+ ),
+
+ TP_fast_assign(
+ __entry->memcg_id = mem_cgroup_id(lruvec_memcg(lruvec));
+ __entry->max_seq = max_seq;
+ __entry->anon_min_seq = anon_min_seq;
+ __entry->file_min_seq = file_min_seq;
+ memcpy(__entry->nr_anon, nr_anon, sizeof(__entry->nr_anon));
+ memcpy(__entry->nr_file, nr_file, sizeof(__entry->nr_file));
+ ),
+
+ TP_printk("memcg_id=%llu max_seq=%lu anon_min_seq=%lu file_min_seq=%lu nr_anon=%s nr_file=%s",
+ __entry->memcg_id,
+ __entry->max_seq,
+ __entry->anon_min_seq,
+ __entry->file_min_seq,
+ __print_array(__entry->nr_anon, MAX_NR_GENS, sizeof(unsigned long)),
+ __print_array(__entry->nr_file, MAX_NR_GENS, sizeof(unsigned long)))
+);
+
TRACE_EVENT(mm_vmscan_write_folio,
TP_PROTO(struct folio *folio),
diff --git a/mm/vmscan.c b/mm/vmscan.c
index 67f59aa73fb9..d1495a7d469d 100644
--- a/mm/vmscan.c
+++ b/mm/vmscan.c
@@ -4100,6 +4100,27 @@ static void try_to_inc_min_seq(struct lruvec *lruvec, int swappiness)
}
}
+static void trace_inc_max_seq(struct lruvec *lruvec)
+{
+ int type, gen;
+ unsigned long nr[ANON_AND_FILE][MAX_NR_GENS];
+ struct lru_gen_folio *lrugen;
+
+ if (!trace_mm_mglru_inc_max_seq_enabled())
+ return;
+
+ lrugen = &lruvec->lrugen;
+ for (type = 0; type < ANON_AND_FILE; type++)
+ for (gen = 0; gen < MAX_NR_GENS; gen++)
+ nr[type][gen] = lru_gen_seq_nr_pages(lrugen, gen, type);
+
+ trace_mm_mglru_inc_max_seq(lruvec,
+ lrugen->max_seq,
+ lrugen->min_seq[LRU_GEN_ANON],
+ lrugen->min_seq[LRU_GEN_FILE],
+ nr[LRU_GEN_ANON], nr[LRU_GEN_FILE]);
+}
+
static bool inc_max_seq(struct lruvec *lruvec, unsigned long seq, int swappiness)
{
bool success;
@@ -4159,6 +4180,8 @@ 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);
+
+ trace_inc_max_seq(lruvec);
unlock:
lruvec_unlock_irq(lruvec);
--
2.34.1
^ permalink raw reply related [flat|nested] 4+ messages in thread
end of thread, other threads:[~2026-09-11 10:30 UTC | newest]
Thread overview: 4+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-09-11 10:29 [PATCH v2 0/3] mm/mglru: add tracepoints for scan and aging paths Ridong Chen
2026-09-11 10:29 ` [PATCH v2 1/3] mm/mglru: factor out lru_gen_seq_nr_pages() Ridong Chen
2026-09-11 10:29 ` [PATCH v2 2/3] mm/mglru: add tracepoint for scan_folios() Ridong Chen
2026-09-11 10:29 ` [PATCH v2 3/3] mm/mglru: add tracepoint for inc_max_seq() Ridong Chen
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox