Re: [RFC PATCH] mm: vmscan: avoid anon scanning for GFP_NOIO with low swapcache

From: Johannes Weiner

Date: Fri Sep 04 2026 - 13:31:28 EST


On Fri, Sep 04, 2026 at 10:07:56AM +0800, Bo Zhang wrote:
> On Thu, Sep 03, 2026 at 09:03:04AM -0400, Johannes Weiner wrote:
> > On Thu, Sep 03, 2026 at 12:01:31PM +0800, Bo Zhang wrote:
> > > We have observed some cases where memory is allocated with GFP_NOIO, so
> > > we cannot reclaim any anon folios unless they are in swapcache. We can
> > > end up spending more than 150 ms looping in `shrink_folio_list()` scanning
> > > non-swapcache folios without reclaiming a single folio. This is pure
> > > overhead.
> >
> > Not entirely. There is some value in aging anon alongside file, so
> > that the next __GFP_IO reclaimer doesn't look at a stale list.
>
> You're right, "pure overhead" was too strong - aging anon does have
> value for a later __GFP_IO reclaimer, and I don't intend to skip it in
> general. Let me describe the case in full, because the reclaim cycle
> itself already provides that aging on a later pass, which is what makes
> me think the trade-off here leans the other way.
>
> > Can you describe a bit more about what you observed? What workload is
> > running, maybe you have a stack trace of which NOIO requests are
> > routinely getting stuck in reclaim?
>
> The workload is app launching on Android. The NOIO allocations come from
> dm-verity hash-block reads via dm-bufio, which legitimately use GFP_NOIO
> because they run underneath the IO path:
>
> worker_thread
> process_scheduled_works
> verity_work
> verity_verify_io
> verity_hash_for_block
> verity_verify_level
> dm_bufio_read_with_ioprio
> new_read
> __bufio_new
> alloc_buffer
> gfp_mask: GFP_NOIO | __GFP_NORETRY | __GFP_NOMEMALLOC | __GFP_NOWARN
>
> So the NOIO use itself is correct; the problem is on the reclaim side.

Ack.

> Here is the full picture of one such direct reclaim. It runs two rounds
> of do_try_to_free_pages(); the target is 32 folios.
>
> Round 1 - partial (shared) memcg walk, 169.20 ms, 0 folios reclaimed
> --------------------------------------------------------------------
> prio 12->1 (~1.3 ms):
> cache_trim_mode is on, so get_scan_count() picks SCAN_FILE. Only the
> file side is scanned. Because this is a shared/partial walk, each
> priority only visits a handful of memcgs before the iterator is
> handed off, so very few memcgs are looked at on the way down:
> 428 file folios scanned, 0 reclaimed.
>
> prio 0 (~167.9 ms):
> priority hits 0 without meeting the target, so get_scan_count()
> forces SCAN_EQUAL. The walk lands on a single memcg with a large,
> unswapped anon LRU and a tiny file LRU:
>
> inactive_anon ~335 MB, inactive_file ~4 MB (~84:1)
> memcg swap usage ~3.6 MB, so swapcache is negligible
>
> shrink_lruvec() now keeps feeding that huge anon list into
> shrink_folio_list() - ~2400 shrink_folio_list() calls, ~93,000 anon
> folios scanned - and every folio hits the !__GFP_IO keep_locked path
> (not in swapcache, needs a swap slot). This single shrink_lruvec()
> pass alone is ~168 ms with 0 folios reclaimed.

Ack. Thanks for the rich explanation, this is illuminating.

Agree with your fix. This GFP_NOIO just has to get through the day,
and aging 90% of memory it cannot reclaim is an unreasonable side
quest. Leave it to kswapd and the other reclaimers.