mirror of https://lore.kernel.org/lkml/
 help / color / mirror / Atom feed
* [PATCH 0/2] mm: kmemleak: batch the struct page scan
@ 2026-09-21 12:30 Breno Leitao
  2026-09-21 12:30 ` [PATCH 1/2] mm: kmemleak: move the struct page scan into a helper Breno Leitao
  2026-09-21 12:30 ` [PATCH 2/2] mm: kmemleak: scan the struct page array in MAX_SCAN_SIZE batches Breno Leitao
  0 siblings, 2 replies; 7+ messages in thread
From: Breno Leitao @ 2026-09-21 12:30 UTC (permalink / raw)
  To: Catalin Marinas, Andrew Morton
  Cc: linux-mm, linux-kernel, Breno Leitao, kernel-team

kmemleak walks the struct page array as a scan root and hands it to
scan_block() one page at a time.

scan_block() takes kmemleak_lock with interrupts disabled for the
duration of the call, so the scanner acquires the lock once per online
PFN in order to look at a single 64-byte struct page.

Batching adjacent pages up to that size keeps exactly the same pages
under the same maximum lock hold time, with MAX_SCAN_SIZE / pagesize
fewer acquisitions.

Patch 1 lifts the loop out of __kmemleak_scan() into scan_zone_pages()
with no functional change. Patch 2 does the batching, which is then
contained in that one function.

This improves the performance due to less atomic operations, which is
not a big deal on a regular machine, but, given kmemleak usually comes
with extra debug options, such as PROVE_LOCKING, DEBUG_SPINLOCK, etc.

For instance, measuring Meta's "debug kernel flavor" on an arm64 hosts,
this improve the scan time by 20%.

It is safe to get more work into scan_block(), given it has the
protections, added by commit eb11f56eeca560 ("mm/kmemleak: stop the task
stack scan early when interrupted")

MAX_SCAN_SIZE is also not a new maximum for a single scan_block() call.
kmemleak_scan_task_stacks() already hands it a whole task stack in one
go. On arm64 and x86_64 THREAD_SIZE is never below 16 KiB, four times
MAX_SCAN_SIZE, and it is 64 KiB on arm64 with 64K pages.

Signed-off-by: Breno Leitao <leitao@debian.org>
---
Breno Leitao (2):
      mm: kmemleak: move the struct page scan into a helper
      mm: kmemleak: scan the struct page array in MAX_SCAN_SIZE batches

 mm/kmemleak.c | 70 +++++++++++++++++++++++++++++++++++++++--------------------
 1 file changed, 47 insertions(+), 23 deletions(-)
---
base-commit: 3f2425f5b5bbbdd991ca9cdfd5502e68d8895998
change-id: 20260918-b4-kmemleak-page-scan-c7ac6b088c4f

Best regards,
--  
Breno Leitao <leitao@debian.org>


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

* [PATCH 1/2] mm: kmemleak: move the struct page scan into a helper
  2026-09-21 12:30 [PATCH 0/2] mm: kmemleak: batch the struct page scan Breno Leitao
@ 2026-09-21 12:30 ` Breno Leitao
  2026-09-22  9:47   ` Catalin Marinas
  2026-09-21 12:30 ` [PATCH 2/2] mm: kmemleak: scan the struct page array in MAX_SCAN_SIZE batches Breno Leitao
  1 sibling, 1 reply; 7+ messages in thread
From: Breno Leitao @ 2026-09-21 12:30 UTC (permalink / raw)
  To: Catalin Marinas, Andrew Morton
  Cc: linux-mm, linux-kernel, Breno Leitao, kernel-team

The struct page scanning loop sits inline in __kmemleak_scan(), nested
three levels deep and sharing the caller's "stop" variable with the
zone walk around it. Move it into scan_zone_pages(), which scans one
zone and returns 1 if scanning should stop.

No functional change; this only makes room for changing how the pages
are handed to scan_block().

Signed-off-by: Breno Leitao <leitao@debian.org>
---
 mm/kmemleak.c | 57 ++++++++++++++++++++++++++++++++++-----------------------
 1 file changed, 34 insertions(+), 23 deletions(-)

diff --git a/mm/kmemleak.c b/mm/kmemleak.c
index 8fa409a4f9fb2..4040547a0af84 100644
--- a/mm/kmemleak.c
+++ b/mm/kmemleak.c
@@ -1855,6 +1855,39 @@ static void dedup_flush(struct xarray *dedup)
 	}
 }
 
+/*
+ * Scan the struct pages of a zone, skipping memory holes, pages that belong to
+ * another zone and pages that are not in use. Returns 1 if the scan should be
+ * stopped.
+ */
+static int scan_zone_pages(struct zone *zone)
+{
+	unsigned long start_pfn = zone->zone_start_pfn;
+	unsigned long end_pfn = zone_end_pfn(zone);
+	unsigned long pfn;
+
+	for (pfn = start_pfn; pfn < end_pfn; pfn++) {
+		struct page *page = pfn_to_online_page(pfn);
+
+		if (!(pfn & 63))
+			cond_resched_tasks_rcu_qs();
+
+		if (!page)
+			continue;
+
+		/* only scan pages belonging to this zone */
+		if (page_zone(page) != zone)
+			continue;
+		/* only scan if page is in use */
+		if (page_count(page) == 0)
+			continue;
+		if (scan_block(page, page + 1, NULL))
+			return 1;
+	}
+
+	return 0;
+}
+
 /*
  * Scan data sections and all the referenced memory blocks allocated via the
  * kernel's standard allocators. This function must be called with the
@@ -1928,29 +1961,7 @@ static int __kmemleak_scan(bool full)
 	 */
 	get_online_mems();
 	for_each_populated_zone(zone) {
-		unsigned long start_pfn = zone->zone_start_pfn;
-		unsigned long end_pfn = zone_end_pfn(zone);
-		unsigned long pfn;
-
-		for (pfn = start_pfn; pfn < end_pfn; pfn++) {
-			struct page *page = pfn_to_online_page(pfn);
-
-			if (!(pfn & 63))
-				cond_resched_tasks_rcu_qs();
-
-			if (!page)
-				continue;
-
-			/* only scan pages belonging to this zone */
-			if (page_zone(page) != zone)
-				continue;
-			/* only scan if page is in use */
-			if (page_count(page) == 0)
-				continue;
-			stop = scan_block(page, page + 1, NULL);
-			if (stop)
-				break;
-		}
+		stop = scan_zone_pages(zone);
 		if (stop)
 			break;
 	}

-- 
2.53.0-Meta


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

* [PATCH 2/2] mm: kmemleak: scan the struct page array in MAX_SCAN_SIZE batches
  2026-09-21 12:30 [PATCH 0/2] mm: kmemleak: batch the struct page scan Breno Leitao
  2026-09-21 12:30 ` [PATCH 1/2] mm: kmemleak: move the struct page scan into a helper Breno Leitao
@ 2026-09-21 12:30 ` Breno Leitao
  2026-09-22  9:51   ` Catalin Marinas
  1 sibling, 1 reply; 7+ messages in thread
From: Breno Leitao @ 2026-09-21 12:30 UTC (permalink / raw)
  To: Catalin Marinas, Andrew Morton
  Cc: linux-mm, linux-kernel, Breno Leitao, kernel-team

scan_zone_pages() scans the struct page array one page per scan_block()
call:

	if (scan_block(page, page + 1, NULL))

scan_block() takes kmemleak_lock with interrupts disabled for the
duration of the call, so this acquires the lock once per online PFN to
scan a single struct page.

Gather runs of adjacent eligible struct pages and pass each run to
scan_block() in one call, capped at MAX_SCAN_SIZE.

The longest kmemleak_lock is now held is MAX_SCAN_SIZE worth of words,
the same bound scan_large_block() already applies to the data sections
and the per-CPU areas.

This can make the scan faster. On an arm64 VM with 24 GiB and ~17 GiB
in use, the struct page phase goes from 4390k scan_block() calls down
to 69k for the same 4390k pages scanned, and the whole scan about 20%
faster (on debug kernel). The win is in the per-acquisition cost, so on
a kernel built without the lock debugging options the scan time is
unchanged. I don't think it will be a problem doing more on
scan_block(), given we have the scan_should_stop() protection.

Coverage is unchanged: a temporary assertion comparing the number of
eligible pages against the number actually passed to scan_block()
matched on every zone of every scan, including while memory was being
freed underneath the scan.

Signed-off-by: Breno Leitao <leitao@debian.org>
---
 mm/kmemleak.c | 29 +++++++++++++++++++++--------
 1 file changed, 21 insertions(+), 8 deletions(-)

diff --git a/mm/kmemleak.c b/mm/kmemleak.c
index 4040547a0af84..cb953df3b414a 100644
--- a/mm/kmemleak.c
+++ b/mm/kmemleak.c
@@ -1862,8 +1862,11 @@ static void dedup_flush(struct xarray *dedup)
  */
 static int scan_zone_pages(struct zone *zone)
 {
+	const unsigned int max_batch = MAX_SCAN_SIZE / sizeof(struct page);
 	unsigned long start_pfn = zone->zone_start_pfn;
 	unsigned long end_pfn = zone_end_pfn(zone);
+	struct page *first = NULL, *last = NULL;
+	unsigned int batch = 0;
 	unsigned long pfn;
 
 	for (pfn = start_pfn; pfn < end_pfn; pfn++) {
@@ -1872,19 +1875,29 @@ static int scan_zone_pages(struct zone *zone)
 		if (!(pfn & 63))
 			cond_resched_tasks_rcu_qs();
 
-		if (!page)
-			continue;
+		/* only scan in-use pages belonging to this zone */
+		if (page && (page_zone(page) != zone ||
+			     page_count(page) == 0))
+			page = NULL;
 
-		/* only scan pages belonging to this zone */
-		if (page_zone(page) != zone)
-			continue;
-		/* only scan if page is in use */
-		if (page_count(page) == 0)
+		if (page && first && page == last + 1 &&
+		    batch < max_batch) {
+			last = page;
+			batch++;
 			continue;
-		if (scan_block(page, page + 1, NULL))
+		}
+
+		if (first && scan_block(first, last + 1, NULL))
 			return 1;
+
+		first = page;
+		last = page;
+		batch = page ? 1 : 0;
 	}
 
+	if (first && scan_block(first, last + 1, NULL))
+		return 1;
+
 	return 0;
 }
 

-- 
2.53.0-Meta


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

* Re: [PATCH 1/2] mm: kmemleak: move the struct page scan into a helper
  2026-09-21 12:30 ` [PATCH 1/2] mm: kmemleak: move the struct page scan into a helper Breno Leitao
@ 2026-09-22  9:47   ` Catalin Marinas
  0 siblings, 0 replies; 7+ messages in thread
From: Catalin Marinas @ 2026-09-22  9:47 UTC (permalink / raw)
  To: Breno Leitao; +Cc: Andrew Morton, linux-mm, linux-kernel, kernel-team

On Mon, Sep 21, 2026 at 05:30:26AM -0700, Breno Leitao wrote:
> The struct page scanning loop sits inline in __kmemleak_scan(), nested
> three levels deep and sharing the caller's "stop" variable with the
> zone walk around it. Move it into scan_zone_pages(), which scans one
> zone and returns 1 if scanning should stop.
> 
> No functional change; this only makes room for changing how the pages
> are handed to scan_block().
> 
> Signed-off-by: Breno Leitao <leitao@debian.org>

Reviewed-by: Catalin Marinas <catalin.marinas@arm.com>

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

* Re: [PATCH 2/2] mm: kmemleak: scan the struct page array in MAX_SCAN_SIZE batches
  2026-09-21 12:30 ` [PATCH 2/2] mm: kmemleak: scan the struct page array in MAX_SCAN_SIZE batches Breno Leitao
@ 2026-09-22  9:51   ` Catalin Marinas
  2026-09-23  1:39     ` Andrew Morton
  0 siblings, 1 reply; 7+ messages in thread
From: Catalin Marinas @ 2026-09-22  9:51 UTC (permalink / raw)
  To: Breno Leitao; +Cc: Andrew Morton, linux-mm, linux-kernel, kernel-team

On Mon, Sep 21, 2026 at 05:30:27AM -0700, Breno Leitao wrote:
> scan_zone_pages() scans the struct page array one page per scan_block()
> call:
> 
> 	if (scan_block(page, page + 1, NULL))
> 
> scan_block() takes kmemleak_lock with interrupts disabled for the
> duration of the call, so this acquires the lock once per online PFN to
> scan a single struct page.
> 
> Gather runs of adjacent eligible struct pages and pass each run to
> scan_block() in one call, capped at MAX_SCAN_SIZE.
> 
> The longest kmemleak_lock is now held is MAX_SCAN_SIZE worth of words,

Remove an 'is'. Also, MAX_SCAN_SIZE worth of bytes rather than words.

Reviewed-by: Catalin Marinas <catalin.marinas@arm.com>

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

* Re: [PATCH 2/2] mm: kmemleak: scan the struct page array in MAX_SCAN_SIZE batches
  2026-09-22  9:51   ` Catalin Marinas
@ 2026-09-23  1:39     ` Andrew Morton
  2026-09-23 10:47       ` Catalin Marinas
  0 siblings, 1 reply; 7+ messages in thread
From: Andrew Morton @ 2026-09-23  1:39 UTC (permalink / raw)
  To: Catalin Marinas; +Cc: Breno Leitao, linux-mm, linux-kernel, kernel-team

On Tue, 22 Sep 2026 10:51:46 +0100 Catalin Marinas <catalin.marinas@arm.com> wrote:

> On Mon, Sep 21, 2026 at 05:30:27AM -0700, Breno Leitao wrote:
> > scan_zone_pages() scans the struct page array one page per scan_block()
> > call:
> > 
> > 	if (scan_block(page, page + 1, NULL))
> > 
> > scan_block() takes kmemleak_lock with interrupts disabled for the
> > duration of the call, so this acquires the lock once per online PFN to
> > scan a single struct page.
> > 
> > Gather runs of adjacent eligible struct pages and pass each run to
> > scan_block() in one call, capped at MAX_SCAN_SIZE.
> > 
> > The longest kmemleak_lock is now held is MAX_SCAN_SIZE worth of words,
> 
> Remove an 'is'.

That was actually correct, but it doesn't read well.

> Also, MAX_SCAN_SIZE worth of bytes rather than words.

edited, thanks.

How's this?

: The longest kmemleak_lock hold duration is now MAX_SCAN_SIZE worth of
: bytes.  The same bound scan_large_block() already applies to the data
: sections and the per-CPU areas.


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

* Re: [PATCH 2/2] mm: kmemleak: scan the struct page array in MAX_SCAN_SIZE batches
  2026-09-23  1:39     ` Andrew Morton
@ 2026-09-23 10:47       ` Catalin Marinas
  0 siblings, 0 replies; 7+ messages in thread
From: Catalin Marinas @ 2026-09-23 10:47 UTC (permalink / raw)
  To: Andrew Morton; +Cc: Breno Leitao, linux-mm, linux-kernel, kernel-team

On Tue, Sep 22, 2026 at 06:39:21PM -0700, Andrew Morton wrote:
> On Tue, 22 Sep 2026 10:51:46 +0100 Catalin Marinas <catalin.marinas@arm.com> wrote:
> 
> > On Mon, Sep 21, 2026 at 05:30:27AM -0700, Breno Leitao wrote:
> > > scan_zone_pages() scans the struct page array one page per scan_block()
> > > call:
> > > 
> > > 	if (scan_block(page, page + 1, NULL))
> > > 
> > > scan_block() takes kmemleak_lock with interrupts disabled for the
> > > duration of the call, so this acquires the lock once per online PFN to
> > > scan a single struct page.
> > > 
> > > Gather runs of adjacent eligible struct pages and pass each run to
> > > scan_block() in one call, capped at MAX_SCAN_SIZE.
> > > 
> > > The longest kmemleak_lock is now held is MAX_SCAN_SIZE worth of words,
> > 
> > Remove an 'is'.
> 
> That was actually correct, but it doesn't read well.

Ah, yes, you can read it in a correct way as well ;).

> > Also, MAX_SCAN_SIZE worth of bytes rather than words.
> 
> edited, thanks.
> 
> How's this?
> 
> : The longest kmemleak_lock hold duration is now MAX_SCAN_SIZE worth of
> : bytes.  The same bound scan_large_block() already applies to the data
> : sections and the per-CPU areas.

Looks good. Thanks!

-- 
Catalin

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

end of thread, other threads:[~2026-09-23 10:47 UTC | newest]

Thread overview: 7+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2026-09-21 12:30 [PATCH 0/2] mm: kmemleak: batch the struct page scan Breno Leitao
2026-09-21 12:30 ` [PATCH 1/2] mm: kmemleak: move the struct page scan into a helper Breno Leitao
2026-09-22  9:47   ` Catalin Marinas
2026-09-21 12:30 ` [PATCH 2/2] mm: kmemleak: scan the struct page array in MAX_SCAN_SIZE batches Breno Leitao
2026-09-22  9:51   ` Catalin Marinas
2026-09-23  1:39     ` Andrew Morton
2026-09-23 10:47       ` Catalin Marinas

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox

all inboxes | Powered by JetHome®