From: Ridong Chen <redacted>
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.
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 | 52 ++++++++++++++---
2 files changed, 145 insertions(+), 8 deletions(-)
--
2.34.1
From: Ridong Chen <redacted>
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 <redacted>
---
mm/vmscan.c | 24 ++++++++++++++++--------
1 file changed, 16 insertions(+), 8 deletions(-)
@@ -2813,6 +2813,18 @@ static int get_nr_gens(struct lruvec *lruvec, int type)returnlruvec->lrugen.max_seq-lruvec->lrugen.min_seq[type]+1;}+/* the number of pages in a generation, summed over zones and clamped to >= 0 */+staticunsignedlonglru_gen_seq_nr_pages(structlru_gen_folio*lrugen,intgen,inttype)+{+intzone;+unsignedlongsize=0;++for(zone=0;zone<MAX_NR_ZONES;zone++)+size+=max(READ_ONCE(lrugen->nr_pages[gen][type][zone]),0L);++returnsize;+}+staticbool__maybe_unusedseq_is_valid(structlruvec*lruvec){inttype;
From: Ridong Chen <redacted>
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 <redacted>
---
include/trace/events/vmscan.h | 63 +++++++++++++++++++++++++++++++++++
mm/vmscan.c | 6 ++++
2 files changed, 69 insertions(+)
From: Ridong Chen <redacted>
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 <redacted>
---
include/trace/events/vmscan.h | 38 +++++++++++++++++++++++++++++++++++
mm/vmscan.c | 22 ++++++++++++++++++++
2 files changed, 60 insertions(+)
Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [Medium] Evaluating pointer dereferences at the tracepoint call site adds unnecessary overhead when tracing is disabled.
--
commit 3d7b5caa2f51b1f8f623f8379f3a020ee141f537
Author: Ridong Chen [off-list ref]
mm/mglru: add tracepoint for scan_folios()
This commit introduces a new MGLRU-specific tracepoint,
mm_mglru_scan_folios, to report scan and isolate counts along with
MGLRU-specific context like memcg ID, generation, and tier. The tracepoint
is placed alongside the classic-LRU tracepoint in scan_folios().
+TRACE_EVENT(mm_mglru_scan_folios,
+
+ TP_PROTO(u64 memcg_id,
+ int highest_zoneidx,
[Severity: Medium]
Does this prototype incorrectly accept an evaluated u64 instead of a pointer?
Accepting the evaluated value here forces evaluation at the call site.
Could we pass the struct lruvec pointer in the prototype instead, so the
expensive pointer dereferences can happen safely inside TP_fast_assign
where they only execute when tracing is enabled?
@@ -4927,6 +4927,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(mem_cgroup_id(lruvec_memcg(lruvec)),+ sc->reclaim_idx, sc->order, nr_to_scan,
[Severity: Medium]
Can this introduce unnecessary overhead on the memory reclaim hot path?
Because trace_mm_mglru_scan_folios evaluates its arguments before checking
the tracepoint's static key, calling mem_cgroup_id(lruvec_memcg(lruvec))
here forces multiple pointer dereferences to happen unconditionally.
The compiler cannot optimize them into the inactive branch because it cannot
prove the arbitrary pointer accesses won't trap. Placing the dereferences
inside TP_fast_assign avoids this execution when tracing is disabled.
On 9/11/2026 3:42 PM, sashiko-bot@kernel.org wrote:
Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [Medium] Evaluating pointer dereferences at the tracepoint call site adds unnecessary overhead when tracing is disabled.
--
commit 3d7b5caa2f51b1f8f623f8379f3a020ee141f537
Author: Ridong Chen [off-list ref]
mm/mglru: add tracepoint for scan_folios()
This commit introduces a new MGLRU-specific tracepoint,
mm_mglru_scan_folios, to report scan and isolate counts along with
MGLRU-specific context like memcg ID, generation, and tier. The tracepoint
is placed alongside the classic-LRU tracepoint in scan_folios().
+TRACE_EVENT(mm_mglru_scan_folios,
+
+ TP_PROTO(u64 memcg_id,
+ int highest_zoneidx,
[Severity: Medium]
Does this prototype incorrectly accept an evaluated u64 instead of a pointer?
Accepting the evaluated value here forces evaluation at the call site.
Could we pass the struct lruvec pointer in the prototype instead, so the
expensive pointer dereferences can happen safely inside TP_fast_assign
where they only execute when tracing is enabled?
@@ -4927,6 +4927,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(mem_cgroup_id(lruvec_memcg(lruvec)),+ sc->reclaim_idx, sc->order, nr_to_scan,
[Severity: Medium]
Can this introduce unnecessary overhead on the memory reclaim hot path?
Because trace_mm_mglru_scan_folios evaluates its arguments before checking
the tracepoint's static key, calling mem_cgroup_id(lruvec_memcg(lruvec))
here forces multiple pointer dereferences to happen unconditionally.
The compiler cannot optimize them into the inactive branch because it cannot
prove the arbitrary pointer accesses won't trap. Placing the dereferences
inside TP_fast_assign avoids this execution when tracing is disabled.
Please keep "int"s together. This creates a structure that is used to write
into the ring buffer. On 64bit machines, the above would add 4 bytes of
padding after each int, whereas:
__field(unsigned long, max_seq)
__field(unsigned long, min_seq)
__field(int, lru)
__field(int, tier)
would not.
@@ -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(mem_cgroup_id(lruvec_memcg(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]);
Can't this information be processed in the tracepoint? That is:
TP_PROTO(struct lruvec *lruvec,
struct scan_control *sc,
struct lru_gen_folio *lrugen,
unsigned long nr_requested,
unsigned long nr_scanned,
unsigned long nr_sorted,
unsigned long nr_skipped,
unsigned long nr_taken,
int type),
TP_ARGS(lruvec, sc, lrugen, nr_requested, nr_scanned,
nr_sorted, nr_skipped, nr_taken, type),
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(unsigned long, max_seq)
__field(unsigned long, min_seq)
__field(int, lru)
__field(int, tier)
),
TP_fast_assign(
__entry->memcg_id = mem_cgroup_id(lruvec_memcg(lruvec));
__entry->highest_zoneidx = sc->reclaim_idx;
__entry->order = sc->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 = type ? LRU_INACTIVE_FILE : LRU_INACTIVE_ANON;
__entry->max_seq = lrugen->max_seq;
__entry->tier = tier;
__entry->min_seq = lrugen->min_seq;
),
This moves the code to generate the parameters into the TP_fast_assign()
which is in a separate text section. It remove code from the work flow
improving instruction cache.
Same can be done for that trace_mm_vmscan_lru_isolate() trace event.
-- Steve
Please keep "int"s together. This creates a structure that is used to write
into the ring buffer. On 64bit machines, the above would add 4 bytes of
padding after each int, whereas:
__field(unsigned long, max_seq)
__field(unsigned long, min_seq)
__field(int, lru)
__field(int, tier)
would not.
@@ -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(mem_cgroup_id(lruvec_memcg(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]);
Can't this information be processed in the tracepoint? That is:
TP_PROTO(struct lruvec *lruvec,
struct scan_control *sc,
struct lru_gen_folio *lrugen,
unsigned long nr_requested,
unsigned long nr_scanned,
unsigned long nr_sorted,
unsigned long nr_skipped,
unsigned long nr_taken,
int type),
TP_ARGS(lruvec, sc, lrugen, nr_requested, nr_scanned,
nr_sorted, nr_skipped, nr_taken, type),
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(unsigned long, max_seq)
__field(unsigned long, min_seq)
__field(int, lru)
__field(int, tier)
),
TP_fast_assign(
__entry->memcg_id = mem_cgroup_id(lruvec_memcg(lruvec));
__entry->highest_zoneidx = sc->reclaim_idx;
__entry->order = sc->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 = type ? LRU_INACTIVE_FILE : LRU_INACTIVE_ANON;
__entry->max_seq = lrugen->max_seq;
__entry->tier = tier;
__entry->min_seq = lrugen->min_seq;
),
This moves the code to generate the parameters into the TP_fast_assign()
which is in a separate text section. It remove code from the work flow
improving instruction cache.
Same can be done for that trace_mm_vmscan_lru_isolate() trace event.