* [PATCH v3 1/4] mm/mglru: add MM_WALK_EMPTY stats and tracepoint
2026-11-13 8:49 [PATCH v3 0/4] skip empty PUD subtrees during aging with a PUD-level Bloom filter Baoquan He
@ 2026-11-13 8:49 ` Baoquan He
2026-11-13 8:53 ` Baoquan He
2026-11-14 3:42 ` sashiko-bot
2026-11-13 8:49 ` [PATCH v3 2/4] mm/mglru: add PUD-level Bloom filter state and generic helpers Baoquan He
` (2 subsequent siblings)
3 siblings, 2 replies; 8+ messages in thread
From: Baoquan He @ 2026-11-13 8:49 UTC (permalink / raw)
To: linux-mm
Cc: akpm, david, kasong, baohua, baolin.wang, qi.zheng, shakeel.butt,
axelrasmussen, yuanchu, weixugc, Baoquan He, Baoquan He
From: Baoquan He <hebaoquan@kylinos.cn>
Add per-walk counters to measure empty aging walks which traverse
an mm's page tables but find no folio for the current lruvec (node+memcg).
These are common on multi-NUMA node systems because lru_gen_use_mm() marks
an mm for all nodes at every context switch.
New counters (accumulated in mm_state->stats[]):
MM_LEAF_ASSOCIATED - leaf entries whose folio is in this lruvec
MM_WALK_TOTAL - page-table walks completed
MM_WALK_EMPTY - walks that found no folio in this lruvec
MM_LEAF_EMPTY_WALKS - leaf entries scanned during empty walks
A new tracepoint, mm_vmscan_lru_gen_walk(), fires after each walk, and the
debugfs lru_gen output ("TYFALWEE") exposes the new counters.
Reviewed-by: Barry Song <baohua@kernel.org>
Signed-off-by: Baoquan He <hebaoquan@kylinos.cn>
Signed-off-by: Baoquan He <baoquan.he@linux.dev>
---
include/linux/mmzone.h | 4 ++++
mm/trace_vmscan.h | 28 ++++++++++++++++++++++++++++
mm/vmscan.c | 32 ++++++++++++++++++++++++++++----
3 files changed, 60 insertions(+), 4 deletions(-)
diff --git a/include/linux/mmzone.h b/include/linux/mmzone.h
index c161d563ce8f..a49efd9b74ed 100644
--- a/include/linux/mmzone.h
+++ b/include/linux/mmzone.h
@@ -579,6 +579,10 @@ enum {
MM_LEAF_YOUNG, /* young leaf entries */
MM_NONLEAF_FOUND, /* non-leaf entries found in Bloom filters */
MM_NONLEAF_ADDED, /* non-leaf entries added to Bloom filters */
+ MM_LEAF_ASSOCIATED, /* leaf entries whose folio is in this lruvec (node+memcg) */
+ MM_WALK_TOTAL, /* page-table walks completed */
+ MM_WALK_EMPTY, /* walks that found no folio in this lruvec */
+ MM_LEAF_EMPTY_WALKS, /* leaf entries scanned during empty walks */
NR_MM_STATS
};
diff --git a/mm/trace_vmscan.h b/mm/trace_vmscan.h
index 565c56428364..4884fc7a35b9 100644
--- a/mm/trace_vmscan.h
+++ b/mm/trace_vmscan.h
@@ -763,6 +763,34 @@ TRACE_EVENT(mm_vmscan_kswapd_clear_hopeless,
__entry->nid,
__print_symbolic(__entry->reason, kswapd_clear_hopeless_reason_ops))
);
+TRACE_EVENT(mm_vmscan_lru_gen_walk,
+
+ TP_PROTO(int nid, unsigned long seq, int leaf_total,
+ int leaf_associated, bool empty),
+
+ TP_ARGS(nid, seq, leaf_total, leaf_associated, empty),
+
+ TP_STRUCT__entry(
+ __field(int, nid)
+ __field(unsigned long, seq)
+ __field(int, leaf_total)
+ __field(int, leaf_associated)
+ __field(bool, empty)
+ ),
+
+ TP_fast_assign(
+ __entry->nid = nid;
+ __entry->seq = seq;
+ __entry->leaf_total = leaf_total;
+ __entry->leaf_associated = leaf_associated;
+ __entry->empty = empty;
+ ),
+
+ TP_printk("nid=%d seq=%lu leaf_total=%d leaf_associated=%d empty=%d",
+ __entry->nid, __entry->seq, __entry->leaf_total,
+ __entry->leaf_associated, __entry->empty)
+);
+
#endif /* _TRACE_VMSCAN_H */
/* This part must be outside protection */
diff --git a/mm/vmscan.c b/mm/vmscan.c
index 96201eae0ceb..98037a562ab6 100644
--- a/mm/vmscan.c
+++ b/mm/vmscan.c
@@ -3590,6 +3590,8 @@ static bool walk_pte_range(pmd_t *pmd, unsigned long start, unsigned long end,
if (!folio)
continue;
+ walk->mm_stats[MM_LEAF_ASSOCIATED]++;
+
if (folio_test_large(folio)) {
const unsigned int max_nr = (end - addr) >> PAGE_SHIFT;
@@ -3688,6 +3690,8 @@ static void walk_pmd_range_locked(pud_t *pud, unsigned long addr, struct vm_area
if (!folio)
goto next;
+ walk->mm_stats[MM_LEAF_ASSOCIATED]++;
+
if (!pmdp_test_and_clear_young_notify(vma, addr, pmd + i))
goto next;
@@ -4151,8 +4155,28 @@ static bool try_to_inc_max_seq(struct lruvec *lruvec, unsigned long seq,
do {
success = iterate_mm_list(walk, &mm);
- if (mm)
+ if (mm) {
+ bool empty = false;
+
walk_mm(mm, walk);
+ /*
+ * A walk that traversed page tables but found no folio
+ * belonging to this lruvec (node+memcg) is pure waste.
+ */
+ if (walk->mm_stats[MM_LEAF_TOTAL]) {
+ walk->mm_stats[MM_WALK_TOTAL]++;
+ if (walk->mm_stats[MM_LEAF_ASSOCIATED] == 0) {
+ walk->mm_stats[MM_WALK_EMPTY]++;
+ walk->mm_stats[MM_LEAF_EMPTY_WALKS] +=
+ walk->mm_stats[MM_LEAF_TOTAL];
+ empty = true;
+ }
+ }
+ trace_mm_vmscan_lru_gen_walk(
+ lruvec_pgdat(lruvec)->node_id, walk->seq,
+ walk->mm_stats[MM_LEAF_TOTAL],
+ walk->mm_stats[MM_LEAF_ASSOCIATED], empty);
+ }
} while (mm);
done:
if (success) {
@@ -5638,14 +5662,14 @@ static void lru_gen_seq_show_full(struct seq_file *m, struct lruvec *lruvec,
seq_puts(m, " ");
for (i = 0; i < NR_MM_STATS; i++) {
- const char *s = "xxxx";
+ const char *s = "xxxxxxxx";
unsigned long n = 0;
if (seq == max_seq && NR_HIST_GENS == 1) {
- s = "TYFA";
+ s = "TYFALWEE";
n = READ_ONCE(mm_state->stats[hist][i]);
} else if (seq != max_seq && NR_HIST_GENS > 1) {
- s = "tyfa";
+ s = "tyfalwee";
n = READ_ONCE(mm_state->stats[hist][i]);
}
--
2.52.0
^ permalink raw reply related [flat|nested] 8+ messages in thread* Re: [PATCH v3 1/4] mm/mglru: add MM_WALK_EMPTY stats and tracepoint
2026-11-13 8:49 ` [PATCH v3 1/4] mm/mglru: add MM_WALK_EMPTY stats and tracepoint Baoquan He
@ 2026-11-13 8:53 ` Baoquan He
2026-11-14 3:42 ` sashiko-bot
1 sibling, 0 replies; 8+ messages in thread
From: Baoquan He @ 2026-11-13 8:53 UTC (permalink / raw)
To: linux-mm
Cc: akpm, david, kasong, baohua, baolin.wang, qi.zheng, shakeel.butt,
axelrasmussen, yuanchu, weixugc, Baoquan He
On 11/13/26 at 04:49pm, Baoquan He wrote:
> From: Baoquan He <hebaoquan@kylinos.cn>
>
> Add per-walk counters to measure empty aging walks which traverse
> an mm's page tables but find no folio for the current lruvec (node+memcg).
> These are common on multi-NUMA node systems because lru_gen_use_mm() marks
> an mm for all nodes at every context switch.
>
> New counters (accumulated in mm_state->stats[]):
>
> MM_LEAF_ASSOCIATED - leaf entries whose folio is in this lruvec
> MM_WALK_TOTAL - page-table walks completed
> MM_WALK_EMPTY - walks that found no folio in this lruvec
> MM_LEAF_EMPTY_WALKS - leaf entries scanned during empty walks
>
> A new tracepoint, mm_vmscan_lru_gen_walk(), fires after each walk, and the
> debugfs lru_gen output ("TYFALWEE") exposes the new counters.
>
> Reviewed-by: Barry Song <baohua@kernel.org>
> Signed-off-by: Baoquan He <hebaoquan@kylinos.cn>
> Signed-off-by: Baoquan He <baoquan.he@linux.dev>
~~~
Git format-patch automatically added my linux.dev mail address, I should
have noticed this and remove it.
> ---
> include/linux/mmzone.h | 4 ++++
> mm/trace_vmscan.h | 28 ++++++++++++++++++++++++++++
> mm/vmscan.c | 32 ++++++++++++++++++++++++++++----
> 3 files changed, 60 insertions(+), 4 deletions(-)
>
> diff --git a/include/linux/mmzone.h b/include/linux/mmzone.h
> index c161d563ce8f..a49efd9b74ed 100644
> --- a/include/linux/mmzone.h
> +++ b/include/linux/mmzone.h
> @@ -579,6 +579,10 @@ enum {
> MM_LEAF_YOUNG, /* young leaf entries */
> MM_NONLEAF_FOUND, /* non-leaf entries found in Bloom filters */
> MM_NONLEAF_ADDED, /* non-leaf entries added to Bloom filters */
> + MM_LEAF_ASSOCIATED, /* leaf entries whose folio is in this lruvec (node+memcg) */
> + MM_WALK_TOTAL, /* page-table walks completed */
> + MM_WALK_EMPTY, /* walks that found no folio in this lruvec */
> + MM_LEAF_EMPTY_WALKS, /* leaf entries scanned during empty walks */
> NR_MM_STATS
> };
>
> diff --git a/mm/trace_vmscan.h b/mm/trace_vmscan.h
> index 565c56428364..4884fc7a35b9 100644
> --- a/mm/trace_vmscan.h
> +++ b/mm/trace_vmscan.h
> @@ -763,6 +763,34 @@ TRACE_EVENT(mm_vmscan_kswapd_clear_hopeless,
> __entry->nid,
> __print_symbolic(__entry->reason, kswapd_clear_hopeless_reason_ops))
> );
> +TRACE_EVENT(mm_vmscan_lru_gen_walk,
> +
> + TP_PROTO(int nid, unsigned long seq, int leaf_total,
> + int leaf_associated, bool empty),
> +
> + TP_ARGS(nid, seq, leaf_total, leaf_associated, empty),
> +
> + TP_STRUCT__entry(
> + __field(int, nid)
> + __field(unsigned long, seq)
> + __field(int, leaf_total)
> + __field(int, leaf_associated)
> + __field(bool, empty)
> + ),
> +
> + TP_fast_assign(
> + __entry->nid = nid;
> + __entry->seq = seq;
> + __entry->leaf_total = leaf_total;
> + __entry->leaf_associated = leaf_associated;
> + __entry->empty = empty;
> + ),
> +
> + TP_printk("nid=%d seq=%lu leaf_total=%d leaf_associated=%d empty=%d",
> + __entry->nid, __entry->seq, __entry->leaf_total,
> + __entry->leaf_associated, __entry->empty)
> +);
> +
> #endif /* _TRACE_VMSCAN_H */
>
> /* This part must be outside protection */
> diff --git a/mm/vmscan.c b/mm/vmscan.c
> index 96201eae0ceb..98037a562ab6 100644
> --- a/mm/vmscan.c
> +++ b/mm/vmscan.c
> @@ -3590,6 +3590,8 @@ static bool walk_pte_range(pmd_t *pmd, unsigned long start, unsigned long end,
> if (!folio)
> continue;
>
> + walk->mm_stats[MM_LEAF_ASSOCIATED]++;
> +
> if (folio_test_large(folio)) {
> const unsigned int max_nr = (end - addr) >> PAGE_SHIFT;
>
> @@ -3688,6 +3690,8 @@ static void walk_pmd_range_locked(pud_t *pud, unsigned long addr, struct vm_area
> if (!folio)
> goto next;
>
> + walk->mm_stats[MM_LEAF_ASSOCIATED]++;
> +
> if (!pmdp_test_and_clear_young_notify(vma, addr, pmd + i))
> goto next;
>
> @@ -4151,8 +4155,28 @@ static bool try_to_inc_max_seq(struct lruvec *lruvec, unsigned long seq,
>
> do {
> success = iterate_mm_list(walk, &mm);
> - if (mm)
> + if (mm) {
> + bool empty = false;
> +
> walk_mm(mm, walk);
> + /*
> + * A walk that traversed page tables but found no folio
> + * belonging to this lruvec (node+memcg) is pure waste.
> + */
> + if (walk->mm_stats[MM_LEAF_TOTAL]) {
> + walk->mm_stats[MM_WALK_TOTAL]++;
> + if (walk->mm_stats[MM_LEAF_ASSOCIATED] == 0) {
> + walk->mm_stats[MM_WALK_EMPTY]++;
> + walk->mm_stats[MM_LEAF_EMPTY_WALKS] +=
> + walk->mm_stats[MM_LEAF_TOTAL];
> + empty = true;
> + }
> + }
> + trace_mm_vmscan_lru_gen_walk(
> + lruvec_pgdat(lruvec)->node_id, walk->seq,
> + walk->mm_stats[MM_LEAF_TOTAL],
> + walk->mm_stats[MM_LEAF_ASSOCIATED], empty);
> + }
> } while (mm);
> done:
> if (success) {
> @@ -5638,14 +5662,14 @@ static void lru_gen_seq_show_full(struct seq_file *m, struct lruvec *lruvec,
>
> seq_puts(m, " ");
> for (i = 0; i < NR_MM_STATS; i++) {
> - const char *s = "xxxx";
> + const char *s = "xxxxxxxx";
> unsigned long n = 0;
>
> if (seq == max_seq && NR_HIST_GENS == 1) {
> - s = "TYFA";
> + s = "TYFALWEE";
> n = READ_ONCE(mm_state->stats[hist][i]);
> } else if (seq != max_seq && NR_HIST_GENS > 1) {
> - s = "tyfa";
> + s = "tyfalwee";
> n = READ_ONCE(mm_state->stats[hist][i]);
> }
>
> --
> 2.52.0
>
^ permalink raw reply [flat|nested] 8+ messages in thread* Re: [PATCH v3 1/4] mm/mglru: add MM_WALK_EMPTY stats and tracepoint
2026-11-13 8:49 ` [PATCH v3 1/4] mm/mglru: add MM_WALK_EMPTY stats and tracepoint Baoquan He
2026-11-13 8:53 ` Baoquan He
@ 2026-11-14 3:42 ` sashiko-bot
1 sibling, 0 replies; 8+ messages in thread
From: sashiko-bot @ 2026-11-14 3:42 UTC (permalink / raw)
To: Baoquan He, linux-mm
Cc: akpm, david, kasong, baohua, baolin.wang, qi.zheng, shakeel.butt,
axelrasmussen, yuanchu, weixugc, Baoquan He
Thank you for your contribution! Sashiko AI review found 3 potential
issues to consider:
- [Medium] mm/mglru: get_pte_pfn() and get_pmd_pfn() skip non-young
entries in walk stats
- [Medium] mm/mglru: walk_pte_range() undercounts MM_LEAF_ASSOCIATED on
large folios
- [Low] mm/mglru: mm_vmscan_lru_gen_walk tracepoint defined outside
CONFIG_LRU_GEN
--
commit b5309c24549f959bf1eb3e88053e43540b30ee90
Author: Baoquan He <hebaoquan@kylinos.cn>
mm/mglru: add MM_WALK_EMPTY stats and tracepoint
This commit adds per-walk statistics and a tracepoint to track MGLRU aging
page-table walks that find no folios belonging to the target lruvec.
> diff --git a/mm/trace_vmscan.h b/mm/trace_vmscan.h
> index 9dafdb5bd3ce8..2b268fd0f89e8 100644
> --- a/mm/trace_vmscan.h
> +++ b/mm/trace_vmscan.h
> @@ -763,6 +763,34 @@ TRACE_EVENT(mm_vmscan_kswapd_clear_hopeless,
> __entry->nid,
> __print_symbolic(__entry->reason, kswapd_clear_hopeless_reason_ops))
> );
> +TRACE_EVENT(mm_vmscan_lru_gen_walk,
> +
> + TP_PROTO(int nid, unsigned long seq, int leaf_total,
> + int leaf_associated, bool empty),
[Severity: Low]
Should mm_vmscan_lru_gen_walk be placed inside the #ifdef CONFIG_LRU_GEN
block alongside mm_mglru_scan_folios and mm_mglru_aging? Its only caller in
try_to_inc_max_seq() is built under CONFIG_LRU_GEN, so defining it here
generates unused tracepoint code and tracefs entries when CONFIG_LRU_GEN is
disabled.
[ ... ]
> diff --git a/mm/vmscan.c b/mm/vmscan.c
> index 25a224746c36d..6034bae03dc21 100644
> --- a/mm/vmscan.c
> +++ b/mm/vmscan.c
> @@ -3582,6 +3582,8 @@ static bool walk_pte_range(pmd_t *pmd, unsigned long start, unsigned long end,
> if (!folio)
> continue;
>
> + walk->mm_stats[MM_LEAF_ASSOCIATED]++;
> +
> if (folio_test_large(folio)) {
> const unsigned int max_nr = (end - addr) >> PAGE_SHIFT;
[Severity: Medium]
Does walk_pte_range() undercount MM_LEAF_ASSOCIATED when a large folio spans
a batch of contiguous PTEs?
When folio_test_large(folio) is true, folio_pte_batch_flags() sets nr to the
number of batched PTEs and adds nr - 1 to walk->mm_stats[MM_LEAF_TOTAL] and
nr to walk->mm_stats[MM_LEAF_YOUNG], before the loop advances by nr entries:
mm/vmscan.c:walk_pte_range() {
...
if (folio_test_large(folio)) {
const unsigned int max_nr = (end - addr) >> PAGE_SHIFT;
nr = folio_pte_batch_flags(folio, NULL, cur_pte, &ptent,
max_nr, FPB_MERGE_YOUNG_DIRTY);
total += nr - 1;
walk->mm_stats[MM_LEAF_TOTAL] += nr - 1;
}
...
young += nr;
walk->mm_stats[MM_LEAF_YOUNG] += nr;
}
Because MM_LEAF_ASSOCIATED is only incremented by 1 before the batch size is
computed, the remaining nr - 1 PTEs in the batch are not counted, and
MM_LEAF_YOUNG can end up larger than MM_LEAF_ASSOCIATED.
[ ... ]
> @@ -4158,8 +4162,28 @@ static bool try_to_inc_max_seq(struct lruvec *lruvec, unsigned long seq,
>
> do {
> success = iterate_mm_list(walk, &mm);
> - if (mm)
> + if (mm) {
> + bool empty = false;
> +
> walk_mm(mm, walk);
> + /*
> + * A walk that traversed page tables but found no folio
> + * belonging to this lruvec (node+memcg) is pure waste.
> + */
> + if (walk->mm_stats[MM_LEAF_TOTAL]) {
> + walk->mm_stats[MM_WALK_TOTAL]++;
> + if (walk->mm_stats[MM_LEAF_ASSOCIATED] == 0) {
> + walk->mm_stats[MM_WALK_EMPTY]++;
> + walk->mm_stats[MM_LEAF_EMPTY_WALKS] +=
> + walk->mm_stats[MM_LEAF_TOTAL];
> + empty = true;
> + }
> + }
> + trace_mm_vmscan_lru_gen_walk(
> + lruvec_pgdat(lruvec)->node_id, walk->seq,
> + walk->mm_stats[MM_LEAF_TOTAL],
> + walk->mm_stats[MM_LEAF_ASSOCIATED], empty);
> + }
> } while (mm);
[Severity: Medium]
Does this count a walk as empty whenever an mm has folios in this lruvec
whose PTEs and PMDs are all non-young?
In walk_pte_range() and walk_pmd_range_locked(), MM_LEAF_ASSOCIATED is
incremented only after get_pte_pfn() or get_pmd_pfn() succeeds, but both
helpers reject non-young entries up front when the mm has no MMU notifiers:
mm/vmscan.c:get_pte_pfn() {
...
if (!pte_young(pte) && !mm_has_notifiers(vma->vm_mm))
return -1;
As a result, non-young leaf entries mapping folios in this lruvec are never
counted in MM_LEAF_ASSOCIATED, and any walk over an mm whose mapped folios in
this lruvec are all cold has MM_LEAF_ASSOCIATED == 0 and is recorded in
MM_WALK_EMPTY, MM_LEAF_EMPTY_WALKS, and trace_mm_vmscan_lru_gen_walk() with
empty=1.
--
Sashiko AI review · https://sashiko.dev/#/patchset/20261113084953.412919-1-baoquan.he@linux.dev?part=1
^ permalink raw reply [flat|nested] 8+ messages in thread
* [PATCH v3 2/4] mm/mglru: add PUD-level Bloom filter state and generic helpers
2026-11-13 8:49 [PATCH v3 0/4] skip empty PUD subtrees during aging with a PUD-level Bloom filter Baoquan He
2026-11-13 8:49 ` [PATCH v3 1/4] mm/mglru: add MM_WALK_EMPTY stats and tracepoint Baoquan He
@ 2026-11-13 8:49 ` Baoquan He
2026-11-13 8:49 ` [PATCH v3 3/4] mm/mglru: skip cold PUD subtrees during aging Baoquan He
2026-11-13 8:49 ` [PATCH v3 4/4] mm/mglru: count PUD subtrees skipped by the PUD-level filter Baoquan He
3 siblings, 0 replies; 8+ messages in thread
From: Baoquan He @ 2026-11-13 8:49 UTC (permalink / raw)
To: linux-mm
Cc: akpm, david, kasong, baohua, baolin.wang, qi.zheng, shakeel.butt,
axelrasmussen, yuanchu, weixugc, Baoquan He, Baoquan He
From: Baoquan He <hebaoquan@kylinos.cn>
Generalize the Bloom filter state and helpers so that another filter
level can reuse the same hash, double-buffering and reset logic.
Replace the PMD-level filter array with a two-dimensional filters array
indexed by level, and add BLOOM_FILTER_PUD and BLOOM_FILTER_PMD. The
PUD-level filters operate at PUD granularity and generalize the
suppression one level up: when all PMDs under a PUD fail the PMD-level
filter, the whole PMD iteration can be skipped. Cross-node empty walks
are the most visible case: a foreign mm's PUDs have no young entries for
this lruvec, so the entire PMD iteration is pure waste.
The existing PMD-level code now goes through the generic
test/update/reset_bloom_filter() helpers.
No behavior change yet; the PUD-level filters are populated and used by
the next patch.
Signed-off-by: Baoquan He <hebaoquan@kylinos.cn>
Signed-off-by: Baoquan He <baoquan.he@linux.dev>
---
include/linux/mmzone.h | 10 ++++++--
mm/vmscan.c | 58 +++++++++++++++++++++++++++++-------------
2 files changed, 49 insertions(+), 19 deletions(-)
diff --git a/include/linux/mmzone.h b/include/linux/mmzone.h
index a49efd9b74ed..1f086ef3a00e 100644
--- a/include/linux/mmzone.h
+++ b/include/linux/mmzone.h
@@ -589,6 +589,12 @@ enum {
/* double-buffering Bloom filters */
#define NR_BLOOM_FILTERS 2
+enum bloom_filter_level {
+ BLOOM_FILTER_PUD,
+ BLOOM_FILTER_PMD,
+ BLOOM_FILTER_LEVELS,
+};
+
struct lru_gen_mm_state {
/* synced with max_seq after each iteration */
unsigned long seq;
@@ -596,8 +602,8 @@ struct lru_gen_mm_state {
struct list_head *head;
/* where the last iteration ended before */
struct list_head *tail;
- /* Bloom filters flip after each iteration */
- unsigned long *filters[NR_BLOOM_FILTERS];
+ /* Bloom filters flip after each iteration, indexed by level */
+ unsigned long *filters[BLOOM_FILTER_LEVELS][NR_BLOOM_FILTERS];
/* the mm stats for debugging */
unsigned long stats[NR_HIST_GENS][NR_MM_STATS];
};
diff --git a/mm/vmscan.c b/mm/vmscan.c
index 98037a562ab6..9dbb3f477d7d 100644
--- a/mm/vmscan.c
+++ b/mm/vmscan.c
@@ -2798,6 +2798,10 @@ static bool __maybe_unused seq_is_valid(struct lruvec *lruvec)
* walk_pmd_range(); the eviction also report them when walking the rmap
* in lru_gen_look_around().
*
+ * A second, coarser pair of filters sits one level up. The filters are
+ * indexed by level: BLOOM_FILTER_PMD for the PMD-level filters above, and
+ * BLOOM_FILTER_PUD for the PUD-level pair.
+ *
* For future optimizations:
* 1. It's not necessary to keep both filters all the time. The spare one can be
* freed after the RCU grace period and reallocated if needed again.
@@ -2823,14 +2827,13 @@ static void get_item_key(void *item, int *key)
key[1] = hash >> BLOOM_FILTER_SHIFT;
}
-static bool test_bloom_filter(struct lru_gen_mm_state *mm_state, unsigned long seq,
- void *item)
+static bool __test_bloom_filter(unsigned long **filters, unsigned long seq, void *item)
{
int key[2];
unsigned long *filter;
int gen = filter_gen_from_seq(seq);
- filter = READ_ONCE(mm_state->filters[gen]);
+ filter = READ_ONCE(filters[gen]);
if (!filter)
return true;
@@ -2839,14 +2842,13 @@ static bool test_bloom_filter(struct lru_gen_mm_state *mm_state, unsigned long s
return test_bit(key[0], filter) && test_bit(key[1], filter);
}
-static void update_bloom_filter(struct lru_gen_mm_state *mm_state, unsigned long seq,
- void *item)
+static void __update_bloom_filter(unsigned long **filters, unsigned long seq, void *item)
{
int key[2];
unsigned long *filter;
int gen = filter_gen_from_seq(seq);
- filter = READ_ONCE(mm_state->filters[gen]);
+ filter = READ_ONCE(filters[gen]);
if (!filter)
return;
@@ -2858,12 +2860,12 @@ static void update_bloom_filter(struct lru_gen_mm_state *mm_state, unsigned long
set_bit(key[1], filter);
}
-static void reset_bloom_filter(struct lru_gen_mm_state *mm_state, unsigned long seq)
+static void __reset_bloom_filter(unsigned long **filters, unsigned long seq)
{
unsigned long *filter;
int gen = filter_gen_from_seq(seq);
- filter = mm_state->filters[gen];
+ filter = filters[gen];
if (filter) {
bitmap_clear(filter, 0, BIT(BLOOM_FILTER_SHIFT));
return;
@@ -2871,7 +2873,25 @@ static void reset_bloom_filter(struct lru_gen_mm_state *mm_state, unsigned long
filter = bitmap_zalloc(BIT(BLOOM_FILTER_SHIFT),
__GFP_HIGH | __GFP_NOMEMALLOC | __GFP_NOWARN);
- WRITE_ONCE(mm_state->filters[gen], filter);
+ WRITE_ONCE(filters[gen], filter);
+}
+
+static bool test_bloom_filter(struct lru_gen_mm_state *mm_state, int level,
+ unsigned long seq, void *item)
+{
+ return __test_bloom_filter(mm_state->filters[level], seq, item);
+}
+
+static void update_bloom_filter(struct lru_gen_mm_state *mm_state, int level,
+ unsigned long seq, void *item)
+{
+ __update_bloom_filter(mm_state->filters[level], seq, item);
+}
+
+static void reset_bloom_filter(struct lru_gen_mm_state *mm_state, int level,
+ unsigned long seq)
+{
+ __reset_bloom_filter(mm_state->filters[level], seq);
}
/******************************************************************************
@@ -3114,7 +3134,7 @@ static bool iterate_mm_list(struct lru_gen_mm_walk *walk, struct mm_struct **ite
spin_unlock(&mm_list->lock);
if (mm && first)
- reset_bloom_filter(mm_state, walk->seq + 1);
+ reset_bloom_filter(mm_state, BLOOM_FILTER_PMD, walk->seq + 1);
if (*iter)
mmdrop(*iter);
@@ -3771,7 +3791,8 @@ static void walk_pmd_range(pud_t *pud, unsigned long start, unsigned long end,
walk_pmd_range_locked(pud, addr, vma, args, bitmap, &first);
}
- if (!walk->force_scan && !test_bloom_filter(mm_state, walk->seq, pmd + i))
+ if (!walk->force_scan &&
+ !test_bloom_filter(mm_state, BLOOM_FILTER_PMD, walk->seq, pmd + i))
continue;
walk->mm_stats[MM_NONLEAF_FOUND]++;
@@ -3782,7 +3803,7 @@ static void walk_pmd_range(pud_t *pud, unsigned long start, unsigned long end,
walk->mm_stats[MM_NONLEAF_ADDED]++;
/* carry over to the next generation */
- update_bloom_filter(mm_state, walk->seq + 1, pmd + i);
+ update_bloom_filter(mm_state, BLOOM_FILTER_PMD, walk->seq + 1, pmd + i);
}
walk_pmd_range_locked(pud, -1, vma, args, bitmap, &first);
@@ -4417,8 +4438,8 @@ bool lru_gen_look_around(struct page_vma_mapped_walk *pvmw, unsigned int nr)
/* feedback from rmap walkers to page table walkers */
if (mm_state && suitable_to_scan(i, young))
- update_bloom_filter(mm_state, READ_ONCE(lruvec->lrugen.max_seq),
- pvmw->pmd);
+ update_bloom_filter(mm_state, BLOOM_FILTER_PMD,
+ READ_ONCE(lruvec->lrugen.max_seq), pvmw->pmd);
mem_cgroup_put(memcg);
@@ -5981,6 +6002,7 @@ void lru_gen_init_memcg(struct mem_cgroup *memcg)
void lru_gen_exit_memcg(struct mem_cgroup *memcg)
{
int i;
+ int level;
int nid;
struct lru_gen_mm_list *mm_list = get_mm_list(memcg);
@@ -5998,9 +6020,11 @@ void lru_gen_exit_memcg(struct mem_cgroup *memcg)
if (!mm_state)
continue;
- for (i = 0; i < NR_BLOOM_FILTERS; i++) {
- bitmap_free(mm_state->filters[i]);
- mm_state->filters[i] = NULL;
+ for (level = 0; level < BLOOM_FILTER_LEVELS; level++) {
+ for (i = 0; i < NR_BLOOM_FILTERS; i++) {
+ bitmap_free(mm_state->filters[level][i]);
+ mm_state->filters[level][i] = NULL;
+ }
}
}
}
--
2.52.0
^ permalink raw reply related [flat|nested] 8+ messages in thread* [PATCH v3 3/4] mm/mglru: skip cold PUD subtrees during aging
2026-11-13 8:49 [PATCH v3 0/4] skip empty PUD subtrees during aging with a PUD-level Bloom filter Baoquan He
2026-11-13 8:49 ` [PATCH v3 1/4] mm/mglru: add MM_WALK_EMPTY stats and tracepoint Baoquan He
2026-11-13 8:49 ` [PATCH v3 2/4] mm/mglru: add PUD-level Bloom filter state and generic helpers Baoquan He
@ 2026-11-13 8:49 ` Baoquan He
2026-11-14 3:42 ` sashiko-bot
2026-11-13 8:49 ` [PATCH v3 4/4] mm/mglru: count PUD subtrees skipped by the PUD-level filter Baoquan He
3 siblings, 1 reply; 8+ messages in thread
From: Baoquan He @ 2026-11-13 8:49 UTC (permalink / raw)
To: linux-mm
Cc: akpm, david, kasong, baohua, baolin.wang, qi.zheng, shakeel.butt,
axelrasmussen, yuanchu, weixugc, Baoquan He, Kairui Song,
Baoquan He
From: Baoquan He <hebaoquan@kylinos.cn>
The aging walks into every PUD and runs the PMD-level Bloom filter on
each PMD. Add a coarser PUD-level filter one level up:
- walk_pmd_range() reports whether the subtree held anything young for
this lruvec. That is the same condition which already carries a PMD
over into the next generation: a young PMD entry, a huge page owned by
this lruvec, or PTEs with enough young ones to pass suitable_to_scan().
- walk_pud_range() records that in the PUD filter, and
- on subsequent generations, skips the whole PUD subtree when the filter
says it had nothing young last generation.
Use (mm, PUD-aligned address) as the filter key rather than the PUD page
table pointer. It is a stable identity for the range being walked, is
available without walking the page tables in lru_gen_look_around(), and
does not depend on the lifetime of a page table page.
The double-buffered filter flips with each new iteration, so newly hot or
migrated-in pages are re-checked promptly rather than suppressed
indefinitely. force_scan walks bypass the PUD test, so manual aging and
newly added mms always rescan and re-populate the filter.
To keep hot regions marked, also report the covering PUD from the rmap
feedback path (lru_gen_look_around()), so regions whose hotness is only
observed by eviction will be re-scanned.
Suggested-by: Kairui Song <ryncsn@gmail.com>
Signed-off-by: Baoquan He <hebaoquan@kylinos.cn>
Signed-off-by: Baoquan He <baoquan.he@linux.dev>
---
mm/vmscan.c | 45 ++++++++++++++++++++++++++++++++++++++-------
1 file changed, 38 insertions(+), 7 deletions(-)
diff --git a/mm/vmscan.c b/mm/vmscan.c
index 9dbb3f477d7d..d12934f2fbb3 100644
--- a/mm/vmscan.c
+++ b/mm/vmscan.c
@@ -3133,8 +3133,10 @@ static bool iterate_mm_list(struct lru_gen_mm_walk *walk, struct mm_struct **ite
spin_unlock(&mm_list->lock);
- if (mm && first)
+ if (mm && first) {
reset_bloom_filter(mm_state, BLOOM_FILTER_PMD, walk->seq + 1);
+ reset_bloom_filter(mm_state, BLOOM_FILTER_PUD, walk->seq + 1);
+ }
if (*iter)
mmdrop(*iter);
@@ -3738,10 +3740,11 @@ static void walk_pmd_range_locked(pud_t *pud, unsigned long addr, struct vm_area
*first = -1;
}
-static void walk_pmd_range(pud_t *pud, unsigned long start, unsigned long end,
+static bool walk_pmd_range(pud_t *pud, unsigned long start, unsigned long end,
struct mm_walk *args)
{
int i;
+ bool young = false;
pmd_t *pmd;
unsigned long next;
unsigned long addr;
@@ -3778,8 +3781,10 @@ static void walk_pmd_range(pud_t *pud, unsigned long start, unsigned long end,
walk->mm_stats[MM_LEAF_TOTAL]++;
- if (pfn != -1)
+ if (pfn != -1) {
walk_pmd_range_locked(pud, addr, vma, args, bitmap, &first);
+ young = true;
+ }
continue;
}
@@ -3789,6 +3794,7 @@ static void walk_pmd_range(pud_t *pud, unsigned long start, unsigned long end,
continue;
walk_pmd_range_locked(pud, addr, vma, args, bitmap, &first);
+ young = true;
}
if (!walk->force_scan &&
@@ -3801,6 +3807,7 @@ static void walk_pmd_range(pud_t *pud, unsigned long start, unsigned long end,
continue;
walk->mm_stats[MM_NONLEAF_ADDED]++;
+ young = true;
/* carry over to the next generation */
update_bloom_filter(mm_state, BLOOM_FILTER_PMD, walk->seq + 1, pmd + i);
@@ -3810,6 +3817,8 @@ static void walk_pmd_range(pud_t *pud, unsigned long start, unsigned long end,
if (i < PTRS_PER_PMD && get_next_vma(PUD_MASK, PMD_SIZE, args, &start, &end))
goto restart;
+
+ return young;
}
static int walk_pud_range(p4d_t *p4d, unsigned long start, unsigned long end,
@@ -3820,6 +3829,7 @@ static int walk_pud_range(p4d_t *p4d, unsigned long start, unsigned long end,
unsigned long addr;
unsigned long next;
struct lru_gen_mm_walk *walk = args->private;
+ struct lru_gen_mm_state *mm_state = get_mm_state(walk->lruvec);
VM_WARN_ON_ONCE(p4d_leaf(*p4d));
@@ -3827,13 +3837,29 @@ static int walk_pud_range(p4d_t *p4d, unsigned long start, unsigned long end,
restart:
for (i = pud_index(start), addr = start; addr != end; i++, addr = next) {
pud_t val = pudp_get(pud + i);
+ void *pud_key;
next = pud_addr_end(addr, end);
if (!pud_present(val) || WARN_ON_ONCE(pud_leaf(val)))
continue;
- walk_pmd_range(pud + i, addr, next, args);
+ /*
+ * Use (mm, PUD-aligned address) as the filter key instead of
+ * the page table pointer. It is stable across the aging walk
+ * and the rmap feedback path, and avoids walking the page
+ * tables in lru_gen_look_around().
+ */
+ pud_key = (void *)((unsigned long)args->mm ^ (addr & PUD_MASK));
+
+ /* Skip a subtree that had nothing young for this lruvec last gen */
+ if (!walk->force_scan &&
+ !test_bloom_filter(mm_state, BLOOM_FILTER_PUD, walk->seq, pud_key))
+ continue;
+
+ if (walk_pmd_range(pud + i, addr, next, args))
+ update_bloom_filter(mm_state, BLOOM_FILTER_PUD, walk->seq + 1,
+ pud_key);
if (need_resched() || walk->batched >= MAX_LRU_BATCH) {
end = (addr | ~PUD_MASK) + 1;
@@ -4437,9 +4463,14 @@ bool lru_gen_look_around(struct page_vma_mapped_walk *pvmw, unsigned int nr)
lazy_mmu_mode_disable();
/* feedback from rmap walkers to page table walkers */
- if (mm_state && suitable_to_scan(i, young))
- update_bloom_filter(mm_state, BLOOM_FILTER_PMD,
- READ_ONCE(lruvec->lrugen.max_seq), pvmw->pmd);
+ if (mm_state && suitable_to_scan(i, young)) {
+ unsigned long max_seq = READ_ONCE(lruvec->lrugen.max_seq);
+ void *pud_key = (void *)((unsigned long)vma->vm_mm ^
+ (pvmw->address & PUD_MASK));
+
+ update_bloom_filter(mm_state, BLOOM_FILTER_PMD, max_seq, pvmw->pmd);
+ update_bloom_filter(mm_state, BLOOM_FILTER_PUD, max_seq, pud_key);
+ }
mem_cgroup_put(memcg);
--
2.52.0
^ permalink raw reply related [flat|nested] 8+ messages in thread* Re: [PATCH v3 3/4] mm/mglru: skip cold PUD subtrees during aging
2026-11-13 8:49 ` [PATCH v3 3/4] mm/mglru: skip cold PUD subtrees during aging Baoquan He
@ 2026-11-14 3:42 ` sashiko-bot
0 siblings, 0 replies; 8+ messages in thread
From: sashiko-bot @ 2026-11-14 3:42 UTC (permalink / raw)
To: Baoquan He, linux-mm
Cc: akpm, david, kasong, baohua, baolin.wang, qi.zheng, shakeel.butt,
axelrasmussen, yuanchu, weixugc, Baoquan He, Kairui Song
Thank you for your contribution! Sashiko AI review found 3 potential
issues to consider:
- [Medium] mm/mglru: walk_pmd_range() marks PUD young on non-leaf PMD
accessed bit
- [Medium] mm/mglru: walk_pmd_range() marks PUD young for THP before
memcg and notifier checks
- [Medium] mm/mglru: PUDs with PMD-mapped THPs are suppressed
indefinitely once skipped
--
commit 31572808d36982038b4b945e98794001c99a5d21
Author: Baoquan He <hebaoquan@kylinos.cn>
mm/mglru: skip cold PUD subtrees during aging
This commit introduces a PUD-level Bloom filter keyed by (mm, PUD-aligned
address) to skip PUD subtrees that had no young pages in the previous
generation during MGLRU aging walks, and updates it from the rmap feedback
path in lru_gen_look_around().
> diff --git a/mm/vmscan.c b/mm/vmscan.c
> index a0332586b3d83..79d533ebfd108 100644
> --- a/mm/vmscan.c
> +++ b/mm/vmscan.c
[ ... ]
> @@ -3730,10 +3732,11 @@ static void walk_pmd_range_locked(pud_t *pud, unsigned long addr, struct vm_area
> *first = -1;
> }
>
> -static void walk_pmd_range(pud_t *pud, unsigned long start, unsigned long end,
> +static bool walk_pmd_range(pud_t *pud, unsigned long start, unsigned long end,
> struct mm_walk *args)
> {
> int i;
> + bool young = false;
> pmd_t *pmd;
> unsigned long next;
> unsigned long addr;
> @@ -3770,8 +3773,10 @@ static void walk_pmd_range(pud_t *pud, unsigned long start, unsigned long end,
>
> walk->mm_stats[MM_LEAF_TOTAL]++;
>
> - if (pfn != -1)
> + if (pfn != -1) {
> walk_pmd_range_locked(pud, addr, vma, args, bitmap, &first);
> + young = true;
> + }
> continue;
> }
[Severity: Medium]
Does setting young = true as soon as get_pmd_pfn() != -1 mark the PUD young
before checking memcg ownership or MMU notifiers?
get_pmd_pfn() only checks that the PFN is on pgdat and that pmd_young() or
mm_has_notifiers() is true, while the memcg check in get_pfn_folio() and
the notifier young check in pmdp_test_and_clear_young_notify() are deferred
to walk_pmd_range_locked(). A PUD with PMD-mapped THPs belonging to
another memcg on the same node, or cold THPs in an mm with MMU notifiers,
will therefore be recorded in BLOOM_FILTER_PUD on every generation and
never skipped.
> @@ -3781,6 +3786,7 @@ static void walk_pmd_range(pud_t *pud, unsigned long start, unsigned long end,
> continue;
>
> walk_pmd_range_locked(pud, addr, vma, args, bitmap, &first);
> + young = true;
> }
>
> if (!walk->force_scan &&
> @@ -3793,6 +3799,7 @@ static void walk_pmd_range(pud_t *pud, unsigned long start, unsigned long end,
> continue;
>
> walk->mm_stats[MM_NONLEAF_ADDED]++;
> + young = true;
>
> /* carry over to the next generation */
> update_bloom_filter(mm_state, BLOOM_FILTER_PMD, walk->seq + 1, pmd + i);
[Severity: Medium]
Should young = true be omitted from the non-leaf pmd_young(val) block?
When should_clear_pmd_young() is true, setting young = true there happens
before checking BLOOM_FILTER_PMD or calling walk_pte_range(). Because CPU
page table walks set the accessed bit on non-leaf PMDs regardless of which
node or memcg owns the underlying PTEs, any accessed non-leaf PMD will
cause walk_pmd_range() to return true and carry the PUD into
BLOOM_FILTER_PUD even when every PMD in the subtree fails the PMD Bloom
filter or has no folios in walk->lruvec. The young = true after
walk_pte_range() already handles non-leaf PMDs that pass suitable_to_scan().
> @@ -3802,6 +3809,8 @@ static void walk_pmd_range(pud_t *pud, unsigned long start, unsigned long end,
>
> if (i < PTRS_PER_PMD && get_next_vma(PUD_MASK, PMD_SIZE, args, &start, &end))
> goto restart;
> +
> + return young;
> }
>
> static int walk_pud_range(p4d_t *p4d, unsigned long start, unsigned long end,
> @@ -3812,6 +3821,7 @@ static int walk_pud_range(p4d_t *p4d, unsigned long start, unsigned long end,
> unsigned long addr;
> unsigned long next;
> struct lru_gen_mm_walk *walk = args->private;
> + struct lru_gen_mm_state *mm_state = get_mm_state(walk->lruvec);
>
> VM_WARN_ON_ONCE(p4d_leaf(*p4d));
>
> @@ -3819,13 +3829,29 @@ static int walk_pud_range(p4d_t *p4d, unsigned long start, unsigned long end,
> restart:
> for (i = pud_index(start), addr = start; addr != end; i++, addr = next) {
> pud_t val = pudp_get(pud + i);
> + void *pud_key;
>
> next = pud_addr_end(addr, end);
>
> if (!pud_present(val) || WARN_ON_ONCE(pud_leaf(val)))
> continue;
>
> - walk_pmd_range(pud + i, addr, next, args);
> + /*
> + * Use (mm, PUD-aligned address) as the filter key instead of
> + * the page table pointer. It is stable across the aging walk
> + * and the rmap feedback path, and avoids walking the page
> + * tables in lru_gen_look_around().
> + */
> + pud_key = (void *)((unsigned long)args->mm ^ (addr & PUD_MASK));
> +
> + /* Skip a subtree that had nothing young for this lruvec last gen */
> + if (!walk->force_scan &&
> + !test_bloom_filter(mm_state, BLOOM_FILTER_PUD, walk->seq, pud_key))
> + continue;
> +
> + if (walk_pmd_range(pud + i, addr, next, args))
> + update_bloom_filter(mm_state, BLOOM_FILTER_PUD, walk->seq + 1,
> + pud_key);
[Severity: Medium]
Once a PUD is absent from BLOOM_FILTER_PUD, can PMD-mapped THPs inside it
ever be scanned by page-table aging again without force_scan?
Previously, BLOOM_FILTER_PMD was checked after pmd_trans_huge() in
walk_pmd_range(), so PMD-mapped THPs were never gated by a Bloom filter.
Gating walk_pmd_range() on BLOOM_FILTER_PUD skips the entire PUD when
pud_key is not present, and iterate_mm_list() clears the next generation's
filter via reset_bloom_filter(mm_state, BLOOM_FILTER_PUD, walk->seq + 1).
The only rmap feedback that populates BLOOM_FILTER_PUD is
lru_gen_look_around(), which folio_referenced_one() only calls when
pvmw.pte is non-NULL:
mm/rmap.c:folio_referenced_one() {
...
if (lru_gen_enabled() && !lru_gen_switching() && pvmw.pte &&
pte_present(pteval)) {
if (lru_gen_look_around(&pvmw, nr))
referenced++;
} else if (pvmw.pte) {
...
} else if (IS_ENABLED(CONFIG_TRANSPARENT_HUGEPAGE)) {
if (pmdp_clear_flush_young_notify(vma, address,
pvmw.pmd))
referenced++;
...
}
For PMD-mapped THPs (pvmw.pte == NULL), rmap eviction checks never update
BLOOM_FILTER_PUD, so any PUD that goes cold for one generation or receives
newly faulted or collapsed THPs after the initial force_scan will be
skipped by aging indefinitely.
--
Sashiko AI review · https://sashiko.dev/#/patchset/20261113084953.412919-1-baoquan.he@linux.dev?part=3
^ permalink raw reply [flat|nested] 8+ messages in thread
* [PATCH v3 4/4] mm/mglru: count PUD subtrees skipped by the PUD-level filter
2026-11-13 8:49 [PATCH v3 0/4] skip empty PUD subtrees during aging with a PUD-level Bloom filter Baoquan He
` (2 preceding siblings ...)
2026-11-13 8:49 ` [PATCH v3 3/4] mm/mglru: skip cold PUD subtrees during aging Baoquan He
@ 2026-11-13 8:49 ` Baoquan He
3 siblings, 0 replies; 8+ messages in thread
From: Baoquan He @ 2026-11-13 8:49 UTC (permalink / raw)
To: linux-mm
Cc: akpm, david, kasong, baohua, baolin.wang, qi.zheng, shakeel.butt,
axelrasmussen, yuanchu, weixugc, Baoquan He, Baoquan He
From: Baoquan He <hebaoquan@kylinos.cn>
Add MM_PUD_EMPTY_SKIPPED, incremented in walk_pud_range() whenever a PUD
subtree is skipped by the filter. Like other counters accumulated
per walk, shown in debugfs lru_gen output (the "S" column of TYFALWEES)
and via the mm_vmscan_lru_gen_walk tracepoint, so the avoided cost
by the filter can be measured directly.
Signed-off-by: Baoquan He <hebaoquan@kylinos.cn>
Signed-off-by: Baoquan He <baoquan.he@linux.dev>
---
include/linux/mmzone.h | 1 +
mm/trace_vmscan.h | 10 ++++++----
mm/vmscan.c | 13 ++++++++-----
3 files changed, 15 insertions(+), 9 deletions(-)
diff --git a/include/linux/mmzone.h b/include/linux/mmzone.h
index 1f086ef3a00e..857ff92d5e58 100644
--- a/include/linux/mmzone.h
+++ b/include/linux/mmzone.h
@@ -583,6 +583,7 @@ enum {
MM_WALK_TOTAL, /* page-table walks completed */
MM_WALK_EMPTY, /* walks that found no folio in this lruvec */
MM_LEAF_EMPTY_WALKS, /* leaf entries scanned during empty walks */
+ MM_PUD_EMPTY_SKIPPED, /* PUD subtrees skipped by the PUD-level filter */
NR_MM_STATS
};
diff --git a/mm/trace_vmscan.h b/mm/trace_vmscan.h
index 4884fc7a35b9..8b31057d66da 100644
--- a/mm/trace_vmscan.h
+++ b/mm/trace_vmscan.h
@@ -766,9 +766,9 @@ TRACE_EVENT(mm_vmscan_kswapd_clear_hopeless,
TRACE_EVENT(mm_vmscan_lru_gen_walk,
TP_PROTO(int nid, unsigned long seq, int leaf_total,
- int leaf_associated, bool empty),
+ int leaf_associated, bool empty, int pud_skipped),
- TP_ARGS(nid, seq, leaf_total, leaf_associated, empty),
+ TP_ARGS(nid, seq, leaf_total, leaf_associated, empty, pud_skipped),
TP_STRUCT__entry(
__field(int, nid)
@@ -776,6 +776,7 @@ TRACE_EVENT(mm_vmscan_lru_gen_walk,
__field(int, leaf_total)
__field(int, leaf_associated)
__field(bool, empty)
+ __field(int, pud_skipped)
),
TP_fast_assign(
@@ -784,11 +785,12 @@ TRACE_EVENT(mm_vmscan_lru_gen_walk,
__entry->leaf_total = leaf_total;
__entry->leaf_associated = leaf_associated;
__entry->empty = empty;
+ __entry->pud_skipped = pud_skipped;
),
- TP_printk("nid=%d seq=%lu leaf_total=%d leaf_associated=%d empty=%d",
+ TP_printk("nid=%d seq=%lu leaf_total=%d leaf_associated=%d empty=%d pud_skipped=%d",
__entry->nid, __entry->seq, __entry->leaf_total,
- __entry->leaf_associated, __entry->empty)
+ __entry->leaf_associated, __entry->empty, __entry->pud_skipped)
);
#endif /* _TRACE_VMSCAN_H */
diff --git a/mm/vmscan.c b/mm/vmscan.c
index d12934f2fbb3..88de6bd4f7ca 100644
--- a/mm/vmscan.c
+++ b/mm/vmscan.c
@@ -3854,8 +3854,10 @@ static int walk_pud_range(p4d_t *p4d, unsigned long start, unsigned long end,
/* Skip a subtree that had nothing young for this lruvec last gen */
if (!walk->force_scan &&
- !test_bloom_filter(mm_state, BLOOM_FILTER_PUD, walk->seq, pud_key))
+ !test_bloom_filter(mm_state, BLOOM_FILTER_PUD, walk->seq, pud_key)) {
+ walk->mm_stats[MM_PUD_EMPTY_SKIPPED]++;
continue;
+ }
if (walk_pmd_range(pud + i, addr, next, args))
update_bloom_filter(mm_state, BLOOM_FILTER_PUD, walk->seq + 1,
@@ -4222,7 +4224,8 @@ static bool try_to_inc_max_seq(struct lruvec *lruvec, unsigned long seq,
trace_mm_vmscan_lru_gen_walk(
lruvec_pgdat(lruvec)->node_id, walk->seq,
walk->mm_stats[MM_LEAF_TOTAL],
- walk->mm_stats[MM_LEAF_ASSOCIATED], empty);
+ walk->mm_stats[MM_LEAF_ASSOCIATED], empty,
+ walk->mm_stats[MM_PUD_EMPTY_SKIPPED]);
}
} while (mm);
done:
@@ -5714,14 +5717,14 @@ static void lru_gen_seq_show_full(struct seq_file *m, struct lruvec *lruvec,
seq_puts(m, " ");
for (i = 0; i < NR_MM_STATS; i++) {
- const char *s = "xxxxxxxx";
+ const char *s = "xxxxxxxxx";
unsigned long n = 0;
if (seq == max_seq && NR_HIST_GENS == 1) {
- s = "TYFALWEE";
+ s = "TYFALWEES";
n = READ_ONCE(mm_state->stats[hist][i]);
} else if (seq != max_seq && NR_HIST_GENS > 1) {
- s = "tyfalwee";
+ s = "tyfalwees";
n = READ_ONCE(mm_state->stats[hist][i]);
}
--
2.52.0
^ permalink raw reply related [flat|nested] 8+ messages in thread