Skip to contentSkip to conversation

mm/zswap: Implement per-cgroup proactive writeback

PATCH Superseded Hao Jia 4 participants 26 messages Jun 18 – Jun 29

Subsystems (9)

v4 of 5 Superseded Latest is v5 →

lore
Share & tools
lore

Hao Jia

author
#0 lore raw

v4 cover mm/zswap: Implement per-cgroup proactive writeback

From: Hao Jia <[email protected]>

Zswap currently writes back pages to backing swap reactively, triggered either by the shrinker or by the pool reaching its size limit. Although proactive memory reclaim can automatically write back a portion of zswap pages via the shrinker, it cannot explicitly control the amount of writeback for a specific memory cgroup. Moreover, proactive memory reclaim may not always be triggered during a steady state.

In certain scenarios, it is desirable to trigger writeback in advance to free up memory. For example, users may want to prepare for an upcoming memory-intensive workload by flushing cold memory to the backing storage when the system is relatively idle.

This patch series introduces a "zswap_writeback_only" key to memory.reclaim cgroup interface, allowing users to proactively write back cold compressed data from zswap to the backing swap device. When specified, this key bypasses standard memory reclaim and exclusively performs proactive zswap writeback up to the requested budget. If omitted, the default reclaim behavior remains unchanged.

Example usage:
# Write back 10MB of compressed data from zswap to the backing swap echo "10M zswap_writeback_only" > memory.reclaim

Patch 1: Extend shrink_memcg() to support batch writeback based on a
compressed-size budget and update its return value semantics, thereby improving the writeback efficiency in the shrink_worker() path.
Patch 2: Extract the memcg iteration and writeback loop into helper
functions to prepare for proactive writeback.
Patch 3: Extend the memory.reclaim cgroup v2 interface with a new
"zswap_writeback_only" key, allowing users to trigger proactive zswap writeback up to a requested budget.
Patch 4: Add the zswpwb_proactive_b stat to track the compressed bytes
of proactive writeback for better monitoring and tuning.
Patch 5:
Add tests for zswap proactive writeback.

v3->v4:
- Drop the per-memcg cursor and keep the root cgroup cursor (zswap_next_shrink) logic intact.
- Stick to using the zswap_writeback_only key, and change the proactive writeback size to use the compressed size.
- Consolidate and reuse the logic between shrink_worker() and shrink_memcg(). Enable batch writeback in the shrink_worker() path, while maintaining a low writeback budget in the zswap_store() path.

v2->v3:
- Align the return value of zswap_proactive_writeback() with memory.reclaim and update the corresponding documentation accordingly.
- Resolve conflicts in test_zswap.c on the mm-unstable branch.
- Enhance the zswap proactive writeback selftests to guard against potential future regressions.

v1->v2:
- As suggested by Yosry and Nhat, extend the memory.reclaim cgroup v2 interface with a "zswap_writeback_only" key instead of adding a new dedicated cgroup interface.
- Update the zswap documentation and add selftests for proactive writeback.

[v3] https://lore.kernel.org/all/[email protected]
[v2] https://lore.kernel.org/all/[email protected]
[v1] https://lore.kernel.org/all/[email protected]

Hao Jia (5):
mm/zswap: Extend shrink_memcg() writeback capability mm/zswap: Factor writeback loop out of shrink_worker() mm/zswap: Implement proactive writeback mm/zswap: Add per-memcg stat for proactive writeback selftests/cgroup: Add tests for zswap proactive writeback

 Documentation/admin-guide/cgroup-v2.rst     |  22 +-
 Documentation/admin-guide/mm/zswap.rst      |  11 +-
 include/linux/memcontrol.h                  |   1 +
 include/linux/zswap.h                       |   7 +
 mm/memcontrol.c                             |   3 +
 mm/vmscan.c                                 |  14 +
 mm/zswap.c                                  | 322 +++++++++++++++-----
 tools/testing/selftests/cgroup/test_zswap.c | 153 +++++++++-
 8 files changed, 456 insertions(+), 77 deletions(-)
Signature · 1 line
2.34.1
author
#1 lore raw

PATCH v4 1/5 mm/zswap: Extend shrink_memcg() writeback capability

From: Hao Jia <[email protected]>

Currently, shrink_memcg() writes back at most one entry per-node during its traversal. This makes shrink_worker() inefficient, as it must repeatedly re-enter shrink_memcg() to make any substantial progress.

To address this, extend shrink_memcg() and rewrite its LRU iteration logic to support batch writeback. Introduce the nr_to_writeback parameter to support a writeback budget based on compressed size. This enables batch writeback in the shrink_worker() path, while maintaining a low writeback budget in the zswap_store() path.

Additionally, to prepare for future proactive writeback, update the return value semantics of shrink_memcg(): a positive value now represents the actual number of compressed bytes written back, 0 indicates that candidates existed but no writeback succeeded, and a negative value represents an error code.

---
 mm/zswap.c | 116 ++++++++++++++++++++++++++++++++++++++++++++---------
 1 file changed, 97 insertions(+), 19 deletions(-)
mm/zswap.c +97 -19
index 761cd699e0a3..d7d031dee4cd 100644
@@ -160,6 +160,11 @@ struct zswap_pool {
 	char tfm_name[CRYPTO_MAX_ALG_NAME];
 };
 
+struct zswap_shrink_walk_arg {
+	unsigned long bytes_written;
+	bool encountered_page_in_swapcache;
+};
+
 /* Global LRU lists shared by all zswap pools. */
 static struct list_lru zswap_list_lru;
 
@@ -1089,8 +1094,9 @@ static enum lru_status shrink_memcg_cb(struct list_head *item, struct list_lru_o
 				       void *arg)
 {
 	struct zswap_entry *entry = container_of(item, struct zswap_entry, lru);
-	bool *encountered_page_in_swapcache = (bool *)arg;
+	struct zswap_shrink_walk_arg *walk_arg = arg;
 	swp_entry_t swpentry;
+	unsigned int length;
 	enum lru_status ret = LRU_REMOVED_RETRY;
 	int writeback_result;
 
@@ -1135,8 +1141,13 @@ static enum lru_status shrink_memcg_cb(struct list_head *item, struct list_lru_o
 	 * Once the lru lock is dropped, the entry might get freed. The
 	 * swpentry is copied to the stack, and entry isn't deref'd again
 	 * until the entry is verified to still be alive in the tree.
+	 *
+	 * entry->length is also copied while the lock is held, because
+	 * zswap_writeback_entry() frees the entry on success and we still
+	 * need its compressed size to account for writeback.
 	 */
 	swpentry = entry->swpentry;
+	length = entry->length;
 
 	/*
 	 * It's safe to drop the lock here because we return either
@@ -1155,12 +1166,13 @@ static enum lru_status shrink_memcg_cb(struct list_head *item, struct list_lru_o
 		 * into the warmer region. We should terminate shrinking (if we're in the dynamic
 		 * shrinker context).
 		 */
-		if (writeback_result == -EEXIST && encountered_page_in_swapcache) {
+		if (writeback_result == -EEXIST) {
 			ret = LRU_STOP;
-			*encountered_page_in_swapcache = true;
+			walk_arg->encountered_page_in_swapcache = true;
 		}
 	} else {
 		zswap_written_back_pages++;
+		walk_arg->bytes_written += length;
 	}
 
 	return ret;
@@ -1169,8 +1181,11 @@ static enum lru_status shrink_memcg_cb(struct list_head *item, struct list_lru_o
 static unsigned long zswap_shrinker_scan(struct shrinker *shrinker,
 		struct shrink_control *sc)
 {
+	struct zswap_shrink_walk_arg walk_arg = {
+		.bytes_written = 0,
+		.encountered_page_in_swapcache = false,
+	};
 	unsigned long shrink_ret;
-	bool encountered_page_in_swapcache = false;
 
 	if (!zswap_shrinker_enabled ||
 			!mem_cgroup_zswap_writeback_enabled(sc->memcg)) {
@@ -1179,9 +1194,9 @@ static unsigned long zswap_shrinker_scan(struct shrinker *shrinker,
 	}
 
 	shrink_ret = list_lru_shrink_walk(&zswap_list_lru, sc, &shrink_memcg_cb,
-		&encountered_page_in_swapcache);
+		&walk_arg);
 
-	if (encountered_page_in_swapcache)
+	if (walk_arg.encountered_page_in_swapcache)
 		return SHRINK_STOP;
 
 	return shrink_ret ? shrink_ret : SHRINK_STOP;
@@ -1275,10 +1290,32 @@ static struct shrinker *zswap_alloc_shrinker(void)
 	return shrinker;
 }
 
-static int shrink_memcg(struct mem_cgroup *memcg)
-{
-	int nid, shrunk = 0, scanned = 0;
+/*
+ * The maximum acceptable scan cost factor for writing back
+ * PAGE_SIZE bytes of compressed data.
+ */
+#define ZSWAP_WB_SCAN_FACTOR	16UL
+#define NR_ZSWAP_WB_BATCH	64UL
 
+/*
+ * Iterate over the per-node zswap LRUs of @memcg in batches, writing back
+ * up to @nr_to_writeback * PAGE_SIZE bytes of compressed data.
+ *
+ * Return: The number of bytes written back, or -ENOENT if @memcg has
+ * writeback disabled, is a zombie cgroup, or has empty zswap LRUs.
+ */
+static long shrink_memcg(struct mem_cgroup *memcg,
+			 unsigned long nr_to_writeback)
+{
+	struct zswap_shrink_walk_arg walk_arg = {
+		.bytes_written = 0,
+		.encountered_page_in_swapcache = false,
+	};
+	u64 bytes_to_writeback = nr_to_writeback << PAGE_SHIFT;
+	bool memcg_list_is_empty = true;
+	int nid;
+
+	/* Memcg with zswap writeback disabled are not candidates. */
 	if (!mem_cgroup_zswap_writeback_enabled(memcg))
 		return -ENOENT;
 
@@ -1290,24 +1327,65 @@ static int shrink_memcg(struct mem_cgroup *memcg)
 		return -ENOENT;
 
 	for_each_node_state(nid, N_NORMAL_MEMORY) {
-		unsigned long nr_to_walk = 1;
+		unsigned long nr_to_scan, nr_scanned = 0;
+		unsigned long remain;
+		walk_arg.encountered_page_in_swapcache = false;
+		/*
+		 * Cap by LRU length: bounds rewalks when referenced
+		 * entries keep rotating to the tail.
+		 */
+		nr_to_scan = list_lru_count_one(&zswap_list_lru, nid, memcg);
+		if (!nr_to_scan)
+			continue;
+		memcg_list_is_empty = false;
+
+		/*
+		 * Cap by SCAN_FACTOR * remain budget: bounds scan cost
+		 * to the remaining writeback budget.
+		 */
+		remain = DIV_ROUND_UP(bytes_to_writeback - walk_arg.bytes_written, PAGE_SIZE);
+		nr_to_scan = min(nr_to_scan,
+				 remain * ZSWAP_WB_SCAN_FACTOR);
 
-		shrunk += list_lru_walk_one(&zswap_list_lru, nid, memcg,
-					    &shrink_memcg_cb, NULL, &nr_to_walk);
-		scanned += 1 - nr_to_walk;
+		while (nr_scanned < nr_to_scan) {
+			unsigned long nr_to_walk = min(NR_ZSWAP_WB_BATCH,
+						       nr_to_scan - nr_scanned);
+
+			/*
+			 * Account for the committed budget rather than the walker's
+			 * actual delta. If the list is emptied concurrently, the
+			 * walker visits nothing and nr_scanned would never advance.
+			 */
+			nr_scanned += nr_to_walk;
+
+			list_lru_walk_one(&zswap_list_lru, nid, memcg,
+					  &shrink_memcg_cb,
+					  &walk_arg,
+					  &nr_to_walk);
+
+			if (walk_arg.bytes_written >= bytes_to_writeback)
+				return walk_arg.bytes_written;
+
+			if (walk_arg.encountered_page_in_swapcache)
+				break;
+
+			cond_resched();
+		}
 	}
 
-	if (!scanned)
+	/* Return -ENOENT if all zswap LRU lists are empty. */
+	if (memcg_list_is_empty)
 		return -ENOENT;
 
-	return shrunk ? 0 : -EAGAIN;
+	return walk_arg.bytes_written;
 }
 
 static void shrink_worker(struct work_struct *w)
 {
 	struct mem_cgroup *memcg;
-	int ret, failures = 0, attempts = 0;
+	int failures = 0, attempts = 0;
 	unsigned long thr;
+	long ret;
 
 	/* Reclaim down to the accept threshold */
 	thr = zswap_accept_thr_pages();
@@ -1368,7 +1446,7 @@ static void shrink_worker(struct work_struct *w)
 			goto resched;
 		}
 
-		ret = shrink_memcg(memcg);
+		ret = shrink_memcg(memcg, NR_ZSWAP_WB_BATCH);
 		/* drop the extra reference */
 		mem_cgroup_put(memcg);
 
@@ -1382,7 +1460,7 @@ static void shrink_worker(struct work_struct *w)
 			continue;
 		++attempts;
 
-		if (ret && ++failures == MAX_RECLAIM_RETRIES)
+		if (ret <= 0 && ++failures == MAX_RECLAIM_RETRIES)
 			break;
 resched:
 		cond_resched();
@@ -1492,7 +1570,7 @@ bool zswap_store(struct folio *folio)
 	objcg = get_obj_cgroup_from_folio(folio);
 	if (objcg && !obj_cgroup_may_zswap(objcg)) {
 		memcg = get_mem_cgroup_from_objcg(objcg);
-		if (shrink_memcg(memcg)) {
+		if (shrink_memcg(memcg, 1) <= 0) {
 			mem_cgroup_put(memcg);
 			goto put_objcg;
 		}
Signature · 1 line
2.34.1
maintainer
#2 lore raw

Re: [PATCH v4 1/5] mm/zswap: Extend shrink_memcg() writeback capability

Quoted 53 lines · Hao Jia

From: Hao Jia <[email protected]>

Currently, shrink_memcg() writes back at most one entry per-node during its traversal. This makes shrink_worker() inefficient, as it must repeatedly re-enter shrink_memcg() to make any substantial progress.

To address this, extend shrink_memcg() and rewrite its LRU iteration logic to support batch writeback. Introduce the nr_to_writeback parameter to support a writeback budget based on compressed size. This enables batch writeback in the shrink_worker() path, while maintaining a low writeback budget in the zswap_store() path.

Additionally, to prepare for future proactive writeback, update the return value semantics of shrink_memcg(): a positive value now represents the actual number of compressed bytes written back, 0 indicates that candidates existed but no writeback succeeded, and a negative value represents an error code.

---
 mm/zswap.c | 116 ++++++++++++++++++++++++++++++++++++++++++++---------
 1 file changed, 97 insertions(+), 19 deletions(-)
mm/zswap.c +7 -1
index 761cd699e0a3..d7d031dee4cd 100644
@@ -160,6 +160,11 @@ struct zswap_pool {
 	char tfm_name[CRYPTO_MAX_ALG_NAME];
 };
 
+struct zswap_shrink_walk_arg {
+	unsigned long bytes_written;
+	bool encountered_page_in_swapcache;
+};
+
 /* Global LRU lists shared by all zswap pools. */
 static struct list_lru zswap_list_lru;
 
@@ -1089,8 +1094,9 @@ static enum lru_status shrink_memcg_cb(struct list_head *item, struct list_lru_o
 				       void *arg)
 {
 	struct zswap_entry *entry = container_of(item, struct zswap_entry, lru);
-	bool *encountered_page_in_swapcache = (bool *)arg;
+	struct zswap_shrink_walk_arg *walk_arg = arg;
 	swp_entry_t swpentry;
+	unsigned int length;
 	enum lru_status ret = LRU_REMOVED_RETRY;
 	int writeback_result;
 

mm/zswap.c static enum lru_status shrink_memcg_cb(struct list_head *item, struct list_lru_o · line 1147

Quoted 7 lines
mm/zswap.c +4 -0
@@ -1135,8 +1141,13 @@ static enum lru_status shrink_memcg_cb(struct list_head *item, struct list_lru_o
 	 * Once the lru lock is dropped, the entry might get freed. The
 	 * swpentry is copied to the stack, and entry isn't deref'd again
 	 * until the entry is verified to still be alive in the tree.
+	 *
+	 * entry->length is also copied while the lock is held, because
+	 * zswap_writeback_entry() frees the entry on success and we still
+	 * need its compressed size to account for writeback.

Hmm that's unnecessary, just update "The swpentry is copied to the stack.." above to "Copy neded fields to the stack.." or something.

Quoted 60 lines
mm/zswap.c +16 -8
 	 */
 	swpentry = entry->swpentry;
+	length = entry->length;
 
 	/*
 	 * It's safe to drop the lock here because we return either
@@ -1155,12 +1166,13 @@ static enum lru_status shrink_memcg_cb(struct list_head *item, struct list_lru_o
 		 * into the warmer region. We should terminate shrinking (if we're in the dynamic
 		 * shrinker context).
 		 */
-		if (writeback_result == -EEXIST && encountered_page_in_swapcache) {
+		if (writeback_result == -EEXIST) {
 			ret = LRU_STOP;
-			*encountered_page_in_swapcache = true;
+			walk_arg->encountered_page_in_swapcache = true;
 		}
 	} else {
 		zswap_written_back_pages++;
+		walk_arg->bytes_written += length;
 	}
 
 	return ret;
@@ -1169,8 +1181,11 @@ static enum lru_status shrink_memcg_cb(struct list_head *item, struct list_lru_o
 static unsigned long zswap_shrinker_scan(struct shrinker *shrinker,
 		struct shrink_control *sc)
 {
+	struct zswap_shrink_walk_arg walk_arg = {
+		.bytes_written = 0,
+		.encountered_page_in_swapcache = false,
+	};
 	unsigned long shrink_ret;
-	bool encountered_page_in_swapcache = false;
 
 	if (!zswap_shrinker_enabled ||
 			!mem_cgroup_zswap_writeback_enabled(sc->memcg)) {
@@ -1179,9 +1194,9 @@ static unsigned long zswap_shrinker_scan(struct shrinker *shrinker,
 	}
 
 	shrink_ret = list_lru_shrink_walk(&zswap_list_lru, sc, &shrink_memcg_cb,
-		&encountered_page_in_swapcache);
+		&walk_arg);
 
-	if (encountered_page_in_swapcache)
+	if (walk_arg.encountered_page_in_swapcache)
 		return SHRINK_STOP;
 
 	return shrink_ret ? shrink_ret : SHRINK_STOP;
@@ -1275,10 +1290,32 @@ static struct shrinker *zswap_alloc_shrinker(void)
 	return shrinker;
 }
 
-static int shrink_memcg(struct mem_cgroup *memcg)
-{
-	int nid, shrunk = 0, scanned = 0;
+/*
+ * The maximum acceptable scan cost factor for writing back
+ * PAGE_SIZE bytes of compressed data.
+ */
+#define ZSWAP_WB_SCAN_FACTOR	16UL
+#define NR_ZSWAP_WB_BATCH	64UL

mm/zswap.c static struct shrinker *zswap_alloc_shrinker(void) · line 1308

Quoted 10 lines
mm/zswap.c +9 -0
 
+/*
+ * Iterate over the per-node zswap LRUs of @memcg in batches, writing back
+ * up to @nr_to_writeback * PAGE_SIZE bytes of compressed data.
+ *
+ * Return: The number of bytes written back, or -ENOENT if @memcg has
+ * writeback disabled, is a zombie cgroup, or has empty zswap LRUs.
+ */
+static long shrink_memcg(struct mem_cgroup *memcg,
+			 unsigned long nr_to_writeback)

Is nr_to_writeback supposed to be the number of pages we want to writeback (regardless of their compressed size), or the compressed bytes we want to writeback divided by PAGE_SIZE?

The way it's being used below seems like it's the latter, but the batch size should be in terms of scanned pages (i.e. uncompressed pages). So this is confusing.

The zswap_store() path expects to reclaim one uncompressed page, but this will reclaim PAGE_SIZE worth of compressed memory when passing 1 IIUC (actually maybe more, see below).

mm/zswap.c static struct shrinker *zswap_alloc_shrinker(void) · line 1318

Quoted 10 lines
mm/zswap.c +10 -0
+{
+	struct zswap_shrink_walk_arg walk_arg = {
+		.bytes_written = 0,
+		.encountered_page_in_swapcache = false,
+	};
+	u64 bytes_to_writeback = nr_to_writeback << PAGE_SHIFT;
+	bool memcg_list_is_empty = true;
+	int nid;
+
+	/* Memcg with zswap writeback disabled are not candidates. */

The comment is unnecessary here, it should be obvious.

Quoted 8 lines
mm/zswap.c +0 -1
 	if (!mem_cgroup_zswap_writeback_enabled(memcg))
 		return -ENOENT;
 
@@ -1290,24 +1327,65 @@ static int shrink_memcg(struct mem_cgroup *memcg)
 		return -ENOENT;
 
 	for_each_node_state(nid, N_NORMAL_MEMORY) {
-		unsigned long nr_to_walk = 1;

mm/zswap.c static int shrink_memcg(struct mem_cgroup *memcg) · line 1339

Quoted 10 lines
mm/zswap.c +10 -0
+		unsigned long nr_to_scan, nr_scanned = 0;
+		unsigned long remain;
+		walk_arg.encountered_page_in_swapcache = false;
+		/*
+		 * Cap by LRU length: bounds rewalks when referenced
+		 * entries keep rotating to the tail.
+		 */
+		nr_to_scan = list_lru_count_one(&zswap_list_lru, nid, memcg);
+		if (!nr_to_scan)
+			continue;

Hmm generally if we are running out of pages to scan then we should scan the rotated entries, and reclaim them on the second pass, right? So this should be working as intended. But I guess this doesn't work well when iterating multiple memcgs, as we don't want to drain referenced entries in one memcg before reclaiming already rotated entries on another.

So I think the assumption here is that the caller will retry if needed, handling balancing scanning between multiple memcgs if needed. Maybe we should document this in the function doc above? We should explain that referenced entries will be rotated but not reclaimed as part of the same call.

mm/zswap.c static int shrink_memcg(struct mem_cgroup *memcg) · line 1348

Quoted 9 lines
mm/zswap.c +9 -0
+		memcg_list_is_empty = false;
+
+		/*
+		 * Cap by SCAN_FACTOR * remain budget: bounds scan cost
+		 * to the remaining writeback budget.
+		 */
+		remain = DIV_ROUND_UP(bytes_to_writeback - walk_arg.bytes_written, PAGE_SIZE);
+		nr_to_scan = min(nr_to_scan,
+				 remain * ZSWAP_WB_SCAN_FACTOR);

For the zswap_store() path bytes_to_writeback=PAGE_SIZE, so remain will initially be 1. But then we multiply by this factor and now to scan 16 pages? Also, where did this factor and equation come from?

We'll also loop over nodes, so we may end up scanning 32 or more pages depending on the number of nodes in the system.

If this is just a heuristic, we should really just start simple and add heuristics later as needed. The caller should probably pass in the number of pages to scan (i.e. uncompressed pages), and leave it to the caller to decide when to retry if the actual memory savings are realized.

Quoted 18 lines
mm/zswap.c +14 -3
-		shrunk += list_lru_walk_one(&zswap_list_lru, nid, memcg,
-					    &shrink_memcg_cb, NULL, &nr_to_walk);
-		scanned += 1 - nr_to_walk;
+		while (nr_scanned < nr_to_scan) {
+			unsigned long nr_to_walk = min(NR_ZSWAP_WB_BATCH,
+						       nr_to_scan - nr_scanned);
+
+			/*
+			 * Account for the committed budget rather than the walker's
+			 * actual delta. If the list is emptied concurrently, the
+			 * walker visits nothing and nr_scanned would never advance.
+			 */
+			nr_scanned += nr_to_walk;
+
+			list_lru_walk_one(&zswap_list_lru, nid, memcg,
+					  &shrink_memcg_cb,
+					  &walk_arg,

mm/zswap.c static int shrink_memcg(struct mem_cgroup *memcg) · line 1373

Quoted 10 lines
mm/zswap.c +10 -0
+					  &nr_to_walk);
+
+			if (walk_arg.bytes_written >= bytes_to_writeback)
+				return walk_arg.bytes_written;
+
+			if (walk_arg.encountered_page_in_swapcache)
+				break;
+
+			cond_resched();
+		}

If the caller is expected to have a retry loop anyway, should we simplify this and just scan each per-node LRU once?

We should also probably bail early if the number of scanned pages has already been reached? Currently shrink_memcg() scans one page at a time, so if it scans a bit more to balance between the nodes it's probably fine.

But with batching, we could end up scanning hundres of extra pages just to balance between all nodes. Is node imbalance a real issue?

Quoted 51 lines
mm/zswap.c +8 -6
 	}
 
-	if (!scanned)
+	/* Return -ENOENT if all zswap LRU lists are empty. */
+	if (memcg_list_is_empty)
 		return -ENOENT;
 
-	return shrunk ? 0 : -EAGAIN;
+	return walk_arg.bytes_written;
 }
 
 static void shrink_worker(struct work_struct *w)
 {
 	struct mem_cgroup *memcg;
-	int ret, failures = 0, attempts = 0;
+	int failures = 0, attempts = 0;
 	unsigned long thr;
+	long ret;
 
 	/* Reclaim down to the accept threshold */
 	thr = zswap_accept_thr_pages();
@@ -1368,7 +1446,7 @@ static void shrink_worker(struct work_struct *w)
 			goto resched;
 		}
 
-		ret = shrink_memcg(memcg);
+		ret = shrink_memcg(memcg, NR_ZSWAP_WB_BATCH);
 		/* drop the extra reference */
 		mem_cgroup_put(memcg);
 
@@ -1382,7 +1460,7 @@ static void shrink_worker(struct work_struct *w)
 			continue;
 		++attempts;
 
-		if (ret && ++failures == MAX_RECLAIM_RETRIES)
+		if (ret <= 0 && ++failures == MAX_RECLAIM_RETRIES)
 			break;
 resched:
 		cond_resched();
@@ -1492,7 +1570,7 @@ bool zswap_store(struct folio *folio)
 	objcg = get_obj_cgroup_from_folio(folio);
 	if (objcg && !obj_cgroup_may_zswap(objcg)) {
 		memcg = get_mem_cgroup_from_objcg(objcg);
-		if (shrink_memcg(memcg)) {
+		if (shrink_memcg(memcg, 1) <= 0) {
 			mem_cgroup_put(memcg);
 			goto put_objcg;
 		}
Signature · 1 line
2.34.1
author
#3 lore raw

Re: [PATCH v4 1/5] mm/zswap: Extend shrink_memcg() writeback capability

Quoted 30 lines · Yosry Ahmed
Quoted 29 lines · Hao Jia

From: Hao Jia <[email protected]>

mm/zswap.c +7 -1
index 761cd699e0a3..d7d031dee4cd 100644
@@ -160,6 +160,11 @@ struct zswap_pool {
  	char tfm_name[CRYPTO_MAX_ALG_NAME];
  };
 
+struct zswap_shrink_walk_arg {
+	unsigned long bytes_written;
+	bool encountered_page_in_swapcache;
+};
+
  /* Global LRU lists shared by all zswap pools. */
  static struct list_lru zswap_list_lru;
 
@@ -1089,8 +1094,9 @@ static enum lru_status shrink_memcg_cb(struct list_head *item, struct list_lru_o
  				       void *arg)
  {
  	struct zswap_entry *entry = container_of(item, struct zswap_entry, lru);
-	bool *encountered_page_in_swapcache = (bool *)arg;
+	struct zswap_shrink_walk_arg *walk_arg = arg;
  	swp_entry_t swpentry;
+	unsigned int length;
  	enum lru_status ret = LRU_REMOVED_RETRY;
  	int writeback_result;
 
Quoted 11 lines

mm/zswap.c static enum lru_status shrink_memcg_cb(struct list_head *item, struct list_lru_o · line 1147

Quoted 8 lines
mm/zswap.c +4 -0
@@ -1135,8 +1141,13 @@ static enum lru_status shrink_memcg_cb(struct list_head *item, struct list_lru_o
  	 * Once the lru lock is dropped, the entry might get freed. The
  	 * swpentry is copied to the stack, and entry isn't deref'd again
  	 * until the entry is verified to still be alive in the tree.
+	 *
+	 * entry->length is also copied while the lock is held, because
+	 * zswap_writeback_entry() frees the entry on success and we still
+	 * need its compressed size to account for writeback.

Hmm that's unnecessary, just update "The swpentry is copied to the stack.." above to "Copy neded fields to the stack.." or something.

I'll do this, thanks.

Quoted 85 lines
Quoted 61 lines
mm/zswap.c +16 -8
  	 */
  	swpentry = entry->swpentry;
+	length = entry->length;
 
  	/*
  	 * It's safe to drop the lock here because we return either
@@ -1155,12 +1166,13 @@ static enum lru_status shrink_memcg_cb(struct list_head *item, struct list_lru_o
  		 * into the warmer region. We should terminate shrinking (if we're in the dynamic
  		 * shrinker context).
  		 */
-		if (writeback_result == -EEXIST && encountered_page_in_swapcache) {
+		if (writeback_result == -EEXIST) {
  			ret = LRU_STOP;
-			*encountered_page_in_swapcache = true;
+			walk_arg->encountered_page_in_swapcache = true;
  		}
  	} else {
  		zswap_written_back_pages++;
+		walk_arg->bytes_written += length;
  	}
 
  	return ret;
@@ -1169,8 +1181,11 @@ static enum lru_status shrink_memcg_cb(struct list_head *item, struct list_lru_o
  static unsigned long zswap_shrinker_scan(struct shrinker *shrinker,
  		struct shrink_control *sc)
  {
+	struct zswap_shrink_walk_arg walk_arg = {
+		.bytes_written = 0,
+		.encountered_page_in_swapcache = false,
+	};
  	unsigned long shrink_ret;
-	bool encountered_page_in_swapcache = false;
 
  	if (!zswap_shrinker_enabled ||
  			!mem_cgroup_zswap_writeback_enabled(sc->memcg)) {
@@ -1179,9 +1194,9 @@ static unsigned long zswap_shrinker_scan(struct shrinker *shrinker,
  	}
 
  	shrink_ret = list_lru_shrink_walk(&zswap_list_lru, sc, &shrink_memcg_cb,
-		&encountered_page_in_swapcache);
+		&walk_arg);
 
-	if (encountered_page_in_swapcache)
+	if (walk_arg.encountered_page_in_swapcache)
  		return SHRINK_STOP;
 
  	return shrink_ret ? shrink_ret : SHRINK_STOP;
@@ -1275,10 +1290,32 @@ static struct shrinker *zswap_alloc_shrinker(void)
  	return shrinker;
  }
 
-static int shrink_memcg(struct mem_cgroup *memcg)
-{
-	int nid, shrunk = 0, scanned = 0;
+/*
+ * The maximum acceptable scan cost factor for writing back
+ * PAGE_SIZE bytes of compressed data.
+ */
+#define ZSWAP_WB_SCAN_FACTOR	16UL
+#define NR_ZSWAP_WB_BATCH	64UL

mm/zswap.c static struct shrinker *zswap_alloc_shrinker(void) · line 1308

Quoted 10 lines
mm/zswap.c +9 -0
 
+/*
+ * Iterate over the per-node zswap LRUs of @memcg in batches, writing back
+ * up to @nr_to_writeback * PAGE_SIZE bytes of compressed data.
+ *
+ * Return: The number of bytes written back, or -ENOENT if @memcg has
+ * writeback disabled, is a zombie cgroup, or has empty zswap LRUs.
+ */
+static long shrink_memcg(struct mem_cgroup *memcg,
+			 unsigned long nr_to_writeback)

Is nr_to_writeback supposed to be the number of pages we want to writeback (regardless of their compressed size), or the compressed bytes we want to writeback divided by PAGE_SIZE?

The way it's being used below seems like it's the latter, but the batch size should be in terms of scanned pages (i.e. uncompressed pages). So this is confusing.

The zswap_store() path expects to reclaim one uncompressed page, but this will reclaim PAGE_SIZE worth of compressed memory when passing 1 IIUC (actually maybe more, see below).

Quoted 12 lines

mm/zswap.c static struct shrinker *zswap_alloc_shrinker(void) · line 1318

Quoted 10 lines
mm/zswap.c +10 -0
+{
+	struct zswap_shrink_walk_arg walk_arg = {
+		.bytes_written = 0,
+		.encountered_page_in_swapcache = false,
+	};
+	u64 bytes_to_writeback = nr_to_writeback << PAGE_SHIFT;
+	bool memcg_list_is_empty = true;
+	int nid;
+
+	/* Memcg with zswap writeback disabled are not candidates. */

The comment is unnecessary here, it should be obvious.

I'll do this, thanks.

Quoted 87 lines
Quoted 9 lines
mm/zswap.c +0 -1
  	if (!mem_cgroup_zswap_writeback_enabled(memcg))
  		return -ENOENT;
 
@@ -1290,24 +1327,65 @@ static int shrink_memcg(struct mem_cgroup *memcg)
  		return -ENOENT;
 
  	for_each_node_state(nid, N_NORMAL_MEMORY) {
-		unsigned long nr_to_walk = 1;

mm/zswap.c static int shrink_memcg(struct mem_cgroup *memcg) · line 1339

Quoted 10 lines
mm/zswap.c +10 -0
+		unsigned long nr_to_scan, nr_scanned = 0;
+		unsigned long remain;
+		walk_arg.encountered_page_in_swapcache = false;
+		/*
+		 * Cap by LRU length: bounds rewalks when referenced
+		 * entries keep rotating to the tail.
+		 */
+		nr_to_scan = list_lru_count_one(&zswap_list_lru, nid, memcg);
+		if (!nr_to_scan)
+			continue;

Hmm generally if we are running out of pages to scan then we should scan the rotated entries, and reclaim them on the second pass, right? So this should be working as intended. But I guess this doesn't work well when iterating multiple memcgs, as we don't want to drain referenced entries in one memcg before reclaiming already rotated entries on another.

So I think the assumption here is that the caller will retry if needed, handling balancing scanning between multiple memcgs if needed. Maybe we should document this in the function doc above? We should explain that referenced entries will be rotated but not reclaimed as part of the same call.

mm/zswap.c static int shrink_memcg(struct mem_cgroup *memcg) · line 1348

Quoted 9 lines
mm/zswap.c +9 -0
+		memcg_list_is_empty = false;
+
+		/*
+		 * Cap by SCAN_FACTOR * remain budget: bounds scan cost
+		 * to the remaining writeback budget.
+		 */
+		remain = DIV_ROUND_UP(bytes_to_writeback - walk_arg.bytes_written, PAGE_SIZE);
+		nr_to_scan = min(nr_to_scan,
+				 remain * ZSWAP_WB_SCAN_FACTOR);

For the zswap_store() path bytes_to_writeback=PAGE_SIZE, so remain will initially be 1. But then we multiply by this factor and now to scan 16 pages? Also, where did this factor and equation come from?

We'll also loop over nodes, so we may end up scanning 32 or more pages depending on the number of nodes in the system.

If this is just a heuristic, we should really just start simple and add heuristics later as needed. The caller should probably pass in the number of pages to scan (i.e. uncompressed pages), and leave it to the caller to decide when to retry if the actual memory savings are realized.

Quoted 18 lines
mm/zswap.c +14 -3
-		shrunk += list_lru_walk_one(&zswap_list_lru, nid, memcg,
-					    &shrink_memcg_cb, NULL, &nr_to_walk);
-		scanned += 1 - nr_to_walk;
+		while (nr_scanned < nr_to_scan) {
+			unsigned long nr_to_walk = min(NR_ZSWAP_WB_BATCH,
+						       nr_to_scan - nr_scanned);
+
+			/*
+			 * Account for the committed budget rather than the walker's
+			 * actual delta. If the list is emptied concurrently, the
+			 * walker visits nothing and nr_scanned would never advance.
+			 */
+			nr_scanned += nr_to_walk;
+
+			list_lru_walk_one(&zswap_list_lru, nid, memcg,
+					  &shrink_memcg_cb,
+					  &walk_arg,

mm/zswap.c static int shrink_memcg(struct mem_cgroup *memcg) · line 1373

Quoted 10 lines
mm/zswap.c +10 -0
+					  &nr_to_walk);
+
+			if (walk_arg.bytes_written >= bytes_to_writeback)
+				return walk_arg.bytes_written;
+
+			if (walk_arg.encountered_page_in_swapcache)
+				break;
+
+			cond_resched();
+		}

If the caller is expected to have a retry loop anyway, should we simplify this and just scan each per-node LRU once?

Quoted 8 lines

We should also probably bail early if the number of scanned pages has already been reached? Currently shrink_memcg() scans one page at a time, so if it scans a bit more to balance between the nodes it's probably fine.

But with batching, we could end up scanning hundres of extra pages just to balance between all nodes. Is node imbalance a real issue?

My initial thought was that if cold memory is evenly distributed across nodes and we are doing a large writeback, it would be better to balance the zswap entry writeback across all nodes rather than just draining node 0 first. However, since we currently lack a proper metric to represent hot/cold memory (such as age-based tracking), doing this probably doesn't make much sense right now.

So, perhaps we want something like this? Please correct me if I'm wrong.

static long shrink_memcg(struct mem_cgroup *memcg,
unsigned long nr_to_scan)
{
struct zswap_shrink_walk_arg walk_arg = {
.bytes_written = 0, .encountered_page_in_swapcache = false,
};
unsigned long nr_remaining = nr_to_scan; bool memcg_list_is_empty = true;
int nid;

if (!mem_cgroup_zswap_writeback_enabled(memcg))

    return -ENOENT;
  if (memcg && !mem_cgroup_online(memcg))
    return -ENOENT;

  for_each_node_state(nid, N_NORMAL_MEMORY) {
    unsigned long nr_to_walk;
    /*
     * Cap the per-node scan by the current LRU length. A referenced
     * entry is only rotated to the tail (second chance) and may be
     * revisited within a single walk; without this cap those rotated
     * entries could drain the shared scan budget on one node.
     */
    nr_to_walk = min(nr_remaining,
         list_lru_count_one(&zswap_list_lru, nid, memcg));
    if (!nr_to_walk)
      continue;
    memcg_list_is_empty = false;

    nr_remaining -= nr_to_walk;
    list_lru_walk_one(&zswap_list_lru, nid, memcg,
          &shrink_memcg_cb, &walk_arg, &nr_to_walk);
    /* Return the unused share of the budget to the pool. */
    nr_remaining += nr_to_walk;

    /* Bail out once the whole scan budget has been spent. */
    if (!nr_remaining)
      break;

    cond_resched();

}

if (memcg_list_is_empty)

    return -ENOENT;

return walk_arg.bytes_written;
}

Thanks,
Hao

maintainer
#4 lore raw

Re: [PATCH v4 1/5] mm/zswap: Extend shrink_memcg() writeback capability

Quoted 6 lines

My initial thought was that if cold memory is evenly distributed across nodes and we are doing a large writeback, it would be better to balance the zswap entry writeback across all nodes rather than just draining node 0 first. However, since we currently lack a proper metric to represent hot/cold memory (such as age-based tracking), doing this probably doesn't make much sense right now.

Yeah let's start simple and go from there.

Quoted 21 lines

So, perhaps we want something like this? Please correct me if I'm wrong.

static long shrink_memcg(struct mem_cgroup *memcg,
unsigned long nr_to_scan)

{
   struct zswap_shrink_walk_arg walk_arg = {
     .bytes_written = 0,
     .encountered_page_in_swapcache = false,
   };
   unsigned long nr_remaining = nr_to_scan;
   bool memcg_list_is_empty = true;
   int nid;

   if (!mem_cgroup_zswap_writeback_enabled(memcg))
     return -ENOENT;

   if (memcg && !mem_cgroup_online(memcg))
     return -ENOENT;

   for_each_node_state(nid, N_NORMAL_MEMORY) {
Quoted 8 lines
     unsigned long nr_to_walk;

     /*
      * Cap the per-node scan by the current LRU length. A referenced
      * entry is only rotated to the tail (second chance) and may be
      * revisited within a single walk; without this cap those rotated
      * entries could drain the shared scan budget on one node.
      */

The comment here is a bit misleading. It's not just about draining one node. One call to shrink_memcg() should only scan entries once. The caller can then choose to scan the memcg again, or scan a different one. In this case, the caller should iterate all memcgs first before retrying memcgs again and reclaiming rotated entries.

Quoted 6 lines
     nr_to_walk = min(nr_remaining,
          list_lru_count_one(&zswap_list_lru, nid, memcg));
     if (!nr_to_walk)
       continue;
     memcg_list_is_empty = false;
Quoted 7 lines
     nr_remaining -= nr_to_walk;
     list_lru_walk_one(&zswap_list_lru, nid, memcg,
           &shrink_memcg_cb, &walk_arg, &nr_to_walk);
     /* Return the unused share of the budget to the pool. */
     nr_remaining += nr_to_walk;

     /* Bail out once the whole scan budget has been spent. */

The comment is unnecessary.

Quoted 4 lines
     if (!nr_remaining)
       break;

     cond_resched();

Did you observe a problem here or did you just add this due to an abundance of caution?

Quoted 3 lines
   }

if (memcg_list_is_empty)

Do we need memcg_list_is_empty? Can we just check if nr_remaining matches nr_to_scan?

Quoted 7 lines
     return -ENOENT;
   return walk_arg.bytes_written;
}

Thanks,
Hao

author
#5 lore raw

Re: [PATCH v4 1/5] mm/zswap: Extend shrink_memcg() writeback capability

Quoted 39 lines · Yosry Ahmed
Quoted 6 lines

My initial thought was that if cold memory is evenly distributed across nodes and we are doing a large writeback, it would be better to balance the zswap entry writeback across all nodes rather than just draining node 0 first. However, since we currently lack a proper metric to represent hot/cold memory (such as age-based tracking), doing this probably doesn't make much sense right now.

Yeah let's start simple and go from there.

Quoted 21 lines

So, perhaps we want something like this? Please correct me if I'm wrong.

static long shrink_memcg(struct mem_cgroup *memcg,
unsigned long nr_to_scan)

{
    struct zswap_shrink_walk_arg walk_arg = {
      .bytes_written = 0,
      .encountered_page_in_swapcache = false,
    };
    unsigned long nr_remaining = nr_to_scan;
    bool memcg_list_is_empty = true;
    int nid;

    if (!mem_cgroup_zswap_writeback_enabled(memcg))
      return -ENOENT;

    if (memcg && !mem_cgroup_online(memcg))
      return -ENOENT;

    for_each_node_state(nid, N_NORMAL_MEMORY) {
Quoted 8 lines
      unsigned long nr_to_walk;

      /*
       * Cap the per-node scan by the current LRU length. A referenced
       * entry is only rotated to the tail (second chance) and may be
       * revisited within a single walk; without this cap those rotated
       * entries could drain the shared scan budget on one node.
       */
Quoted 5 lines

The comment here is a bit misleading. It's not just about draining one node. One call to shrink_memcg() should only scan entries once. The caller can then choose to scan the memcg again, or scan a different one. In this case, the caller should iterate all memcgs first before retrying memcgs again and reclaiming rotated entries.

I have updated the comment. Please see below.

Quoted 13 lines
Quoted 13 lines
      nr_to_walk = min(nr_remaining,
           list_lru_count_one(&zswap_list_lru, nid, memcg));
      if (!nr_to_walk)
        continue;
      memcg_list_is_empty = false;

      nr_remaining -= nr_to_walk;
      list_lru_walk_one(&zswap_list_lru, nid, memcg,
            &shrink_memcg_cb, &walk_arg, &nr_to_walk);
      /* Return the unused share of the budget to the pool. */
      nr_remaining += nr_to_walk;
Quoted 3 lines
Quoted 1 line
      /* Bail out once the whole scan budget has been spent. */

The comment is unnecessary.

I'll do this, thanks.

Quoted 8 lines
Quoted 5 lines
      if (!nr_remaining)
        break;

      cond_resched();

Did you observe a problem here or did you just add this due to an abundance of caution?

The cond_resched() here was just out of caution. Given that both callers (shrink_worker() and zswap_proactive_writeback()) already have rescheduling checks, I suppose we can remove it from here."

Quoted 8 lines
Quoted 4 lines
    }

    if (memcg_list_is_empty)

Do we need memcg_list_is_empty? Can we just check if nr_remaining matches nr_to_scan?

indeed.

Quoted 4 lines
Quoted 4 lines
      return -ENOENT;

    return walk_arg.bytes_written;
}

/*
* Scan up to @nr_to_scan pages across the per-node zswap LRUs of @memcg
* and write back the reclaimable ones.
*
* Since the second-chance algorithm rotates referenced entries to the
* LRU tail, the per-node scan is capped at the current LRU length so
* each entry is scanned at most once per call. It is up to the caller
* to handle retries, deciding whether to scan the next memcg to complete
* the full iteration, or to rescan the current memcg to drain its zswap
* entries.
*
* Return: The number of compressed bytes written back (>= 0), or -ENOENT
* if @memcg has writeback disabled, is a zombie cgroup, or has empty
* zswap LRUs.
*/
static long shrink_memcg(struct mem_cgroup *memcg, unsigned long nr_to_scan)

{
    struct zswap_shrink_walk_arg walk_arg = {
        .bytes_written = 0,
        .encountered_page_in_swapcache = false,
    };
    unsigned long nr_remaining = nr_to_scan;
    int nid;

    if (!mem_cgroup_zswap_writeback_enabled(memcg))
        return -ENOENT;

    /*
     * Skip zombies because their LRUs are reparented and we would be
     * reclaiming from the parent instead of the dead memcg.
     */
    if (memcg && !mem_cgroup_online(memcg))
        return -ENOENT;

    for_each_node_state(nid, N_NORMAL_MEMORY) {
        unsigned long nr_to_walk;

        /*
         * Cap the walk at the current LRU length to ensure each entry is
         * scanned at most once per call. Referenced entries are rotated
         * to the tail for a second chance, and this bound prevents them
         * from being revisited within a single call. Retries are left to
         * the caller, which can choose to rescan the current memcg or
         * move on to the next one.
         */
        nr_to_walk = min(nr_remaining,
                 list_lru_count_one(&zswap_list_lru, nid, memcg));
        if (!nr_to_walk)
            continue;

        nr_remaining -= nr_to_walk;
        list_lru_walk_one(&zswap_list_lru, nid, memcg, &shrink_memcg_cb,
                  &walk_arg, &nr_to_walk);
        /* Return the unused share of the budget to the pool. */
        nr_remaining += nr_to_walk;

        if (!nr_remaining)
            break;
    }

    /* Nothing was scanned: every LRU under @memcg was empty. */
    if (nr_remaining == nr_to_scan)
        return -ENOENT;

    return walk_arg.bytes_written;
}

Thanks,
Hao

maintainer
#6 lore raw

Re: [PATCH v4 1/5] mm/zswap: Extend shrink_memcg() writeback capability

Quoted 9 lines

/*
* Scan up to @nr_to_scan pages across the per-node zswap LRUs of @memcg
* and write back the reclaimable ones.
*
* Since the second-chance algorithm rotates referenced entries to the
* LRU tail, the per-node scan is capped at the current LRU length so
* each entry is scanned at most once per call. It is up to the caller
* to handle retries, deciding whether to scan the next memcg to complete

Nit: "whether to scan another memcg to complete.."

Quoted 29 lines

* the full iteration, or to rescan the current memcg to drain its zswap
* entries.
*
* Return: The number of compressed bytes written back (>= 0), or -ENOENT
* if @memcg has writeback disabled, is a zombie cgroup, or has empty
* zswap LRUs.
*/
static long shrink_memcg(struct mem_cgroup *memcg, unsigned long nr_to_scan)

{
     struct zswap_shrink_walk_arg walk_arg = {
         .bytes_written = 0,
         .encountered_page_in_swapcache = false,
     };
     unsigned long nr_remaining = nr_to_scan;
     int nid;

     if (!mem_cgroup_zswap_writeback_enabled(memcg))
         return -ENOENT;

     /*
      * Skip zombies because their LRUs are reparented and we would be
      * reclaiming from the parent instead of the dead memcg.
      */
     if (memcg && !mem_cgroup_online(memcg))
         return -ENOENT;

     for_each_node_state(nid, N_NORMAL_MEMORY) {
         unsigned long nr_to_walk;
Quoted 8 lines
         /*
          * Cap the walk at the current LRU length to ensure each entry is
          * scanned at most once per call. Referenced entries are rotated
          * to the tail for a second chance, and this bound prevents them
          * from being revisited within a single call. Retries are left to
          * the caller, which can choose to rescan the current memcg or
          * move on to the next one.
          */

Nit: Make this more concise since it's already explained above.

Otherwise this looks good to me, thank you!

Quoted 25 lines
         nr_to_walk = min(nr_remaining,
                  list_lru_count_one(&zswap_list_lru, nid, memcg));
         if (!nr_to_walk)
             continue;

         nr_remaining -= nr_to_walk;
         list_lru_walk_one(&zswap_list_lru, nid, memcg, &shrink_memcg_cb,
                   &walk_arg, &nr_to_walk);
         /* Return the unused share of the budget to the pool. */
         nr_remaining += nr_to_walk;

         if (!nr_remaining)
             break;
     }

     /* Nothing was scanned: every LRU under @memcg was empty. */
     if (nr_remaining == nr_to_scan)
         return -ENOENT;

     return walk_arg.bytes_written;
}

Thanks,
Hao

author
#7 lore raw

Re: [PATCH v4 1/5] mm/zswap: Extend shrink_memcg() writeback capability

Quoted 11 lines · Yosry Ahmed
Quoted 9 lines

/*
* Scan up to @nr_to_scan pages across the per-node zswap LRUs of @memcg
* and write back the reclaimable ones.
*
* Since the second-chance algorithm rotates referenced entries to the
* LRU tail, the per-node scan is capped at the current LRU length so
* each entry is scanned at most once per call. It is up to the caller
* to handle retries, deciding whether to scan the next memcg to complete

Nit: "whether to scan another memcg to complete.."

Will fix in the next version.

Quoted 39 lines
Quoted 30 lines

* the full iteration, or to rescan the current memcg to drain its zswap
* entries.
*
* Return: The number of compressed bytes written back (>= 0), or -ENOENT
* if @memcg has writeback disabled, is a zombie cgroup, or has empty
* zswap LRUs.
*/
static long shrink_memcg(struct mem_cgroup *memcg, unsigned long nr_to_scan)

{
      struct zswap_shrink_walk_arg walk_arg = {
          .bytes_written = 0,
          .encountered_page_in_swapcache = false,
      };
      unsigned long nr_remaining = nr_to_scan;
      int nid;

      if (!mem_cgroup_zswap_writeback_enabled(memcg))
          return -ENOENT;

      /*
       * Skip zombies because their LRUs are reparented and we would be
       * reclaiming from the parent instead of the dead memcg.
       */
      if (memcg && !mem_cgroup_online(memcg))
          return -ENOENT;

      for_each_node_state(nid, N_NORMAL_MEMORY) {
          unsigned long nr_to_walk;
Quoted 8 lines
          /*
           * Cap the walk at the current LRU length to ensure each entry is
           * scanned at most once per call. Referenced entries are rotated
           * to the tail for a second chance, and this bound prevents them
           * from being revisited within a single call. Retries are left to
           * the caller, which can choose to rescan the current memcg or
           * move on to the next one.
           */
Quoted 2 lines

Nit: Make this more concise since it's already explained above.

Will fix in the next version. Thanks a lot for the review!

Thanks,
Hao

Quoted 24 lines

Otherwise this looks good to me, thank you!

Quoted 22 lines
          nr_to_walk = min(nr_remaining,
                   list_lru_count_one(&zswap_list_lru, nid, memcg));
          if (!nr_to_walk)
              continue;

          nr_remaining -= nr_to_walk;
          list_lru_walk_one(&zswap_list_lru, nid, memcg, &shrink_memcg_cb,
                    &walk_arg, &nr_to_walk);
          /* Return the unused share of the budget to the pool. */
          nr_remaining += nr_to_walk;

          if (!nr_remaining)
              break;
      }

      /* Nothing was scanned: every LRU under @memcg was empty. */
      if (nr_remaining == nr_to_scan)
          return -ENOENT;

      return walk_arg.bytes_written;
}
author
#8 lore raw

PATCH v4 2/5 mm/zswap: Factor writeback loop out of shrink_worker()

In reply to Hao Jia

From: Hao Jia <[email protected]>

In preparation for sharing the writeback loop with proactive writeback, move the memcg iteration into zswap_iter_global() and the loop into zswap_try_to_writeback(lower, upper). shrink_worker() is reduced to computing the accept threshold and invoking the helper.

---
 mm/zswap.c | 136 +++++++++++++++++++++++++++++++----------------------
 1 file changed, 81 insertions(+), 55 deletions(-)
mm/zswap.c +81 -55
index d7d031dee4cd..e29f8a61412d 100644
@@ -1380,61 +1380,75 @@ static long shrink_memcg(struct mem_cgroup *memcg,
 	return walk_arg.bytes_written;
 }
 
-static void shrink_worker(struct work_struct *w)
+/*
+ * Global iteration uses a global cursor to select from all online
+ * memcgs in a round-robin fashion.
+ *
+ * We save iteration cursor memcg into zswap_next_shrink,
+ * which can be modified by the offline memcg cleaner
+ * zswap_memcg_offline_cleanup().
+ *
+ * Since the offline cleaner is called only once, we cannot leave an
+ * offline memcg reference in zswap_next_shrink.
+ * We can rely on the cleaner only if we get online memcg under lock.
+ *
+ * If we get an offline memcg, we cannot determine if the cleaner has
+ * already been called or will be called later. We must put back the
+ * reference before returning from this function. Otherwise, the
+ * offline memcg left in zswap_next_shrink will hold the reference
+ * until the next run of shrink_worker().
+ */
+static struct mem_cgroup *zswap_iter_global(void)
 {
 	struct mem_cgroup *memcg;
-	int failures = 0, attempts = 0;
-	unsigned long thr;
-	long ret;
-
-	/* Reclaim down to the accept threshold */
-	thr = zswap_accept_thr_pages();
 
 	/*
-	 * Global reclaim will select cgroup in a round-robin fashion from all
-	 * online memcgs, but memcgs that have no pages in zswap and
-	 * writeback-disabled memcgs (memory.zswap.writeback=0) are not
-	 * candidates for shrinking.
+	 * Start from the next memcg after zswap_next_shrink.
+	 * When the offline cleaner has already advanced the cursor,
+	 * advancing the cursor here overlooks one memcg, but this
+	 * should be negligibly rare.
 	 *
-	 * Shrinking will be aborted if we encounter the following
-	 * MAX_RECLAIM_RETRIES times:
-	 * - No writeback-candidate memcgs found in a memcg tree walk.
-	 * - Shrinking a writeback-candidate memcg failed.
-	 *
-	 * We save iteration cursor memcg into zswap_next_shrink,
-	 * which can be modified by the offline memcg cleaner
-	 * zswap_memcg_offline_cleanup().
-	 *
-	 * Since the offline cleaner is called only once, we cannot leave an
-	 * offline memcg reference in zswap_next_shrink.
-	 * We can rely on the cleaner only if we get online memcg under lock.
-	 *
-	 * If we get an offline memcg, we cannot determine if the cleaner has
-	 * already been called or will be called later. We must put back the
-	 * reference before returning from this function. Otherwise, the
-	 * offline memcg left in zswap_next_shrink will hold the reference
-	 * until the next run of shrink_worker().
+	 * If we get an online memcg, keep the extra reference in case
+	 * the original one obtained by mem_cgroup_iter() is dropped by
+	 * zswap_memcg_offline_cleanup() while we are shrinking the
+	 * memcg.
 	 */
+	spin_lock(&zswap_shrink_lock);
 	do {
-		/*
-		 * Start shrinking from the next memcg after zswap_next_shrink.
-		 * When the offline cleaner has already advanced the cursor,
-		 * advancing the cursor here overlooks one memcg, but this
-		 * should be negligibly rare.
-		 *
-		 * If we get an online memcg, keep the extra reference in case
-		 * the original one obtained by mem_cgroup_iter() is dropped by
-		 * zswap_memcg_offline_cleanup() while we are shrinking the
-		 * memcg.
-		 */
-		spin_lock(&zswap_shrink_lock);
-		do {
-			memcg = mem_cgroup_iter(NULL, zswap_next_shrink, NULL);
-			zswap_next_shrink = memcg;
-		} while (memcg && !mem_cgroup_tryget_online(memcg));
-		spin_unlock(&zswap_shrink_lock);
+		memcg = mem_cgroup_iter(NULL, zswap_next_shrink, NULL);
+		zswap_next_shrink = memcg;
+	} while (memcg && !mem_cgroup_tryget_online(memcg));
+	spin_unlock(&zswap_shrink_lock);
+
+	return memcg;
+}
+
+/*
+ * Walk the memcg tree and write back zswap pages until the
+ * (lower_pages, upper_pages) window closes, or abort encounter
+ * MAX_RECLAIM_RETRIES times of the following conditions:
+ * - No writeback-candidate memcgs found in a memcg tree walk.
+ * - Shrinking a writeback-candidate memcg failed.
+ *
+ * For shrink_worker(), it passes lower=thr and upper=zswap_total_pages().
+ * The @upper limit is refreshed in each iteration by re-evaluating
+ * zswap_total_pages(), and the window closes once the total falls
+ * below the threshold.
+ */
+static void zswap_try_to_writeback(unsigned long lower_pages,
+				   unsigned long upper_pages)
+{
+	int failures = 0, attempts = 0;
+	struct mem_cgroup *iter_memcg;
+
+	while (lower_pages < upper_pages) {
+		unsigned long batch_size;
+		long shrunk;
 
-		if (!memcg) {
+		cond_resched();
+
+		iter_memcg = zswap_iter_global();
+		if (!iter_memcg) {
 			/*
 			 * Continue shrinking without incrementing failures if
 			 * we found candidate memcgs in the last tree walk.
@@ -1443,12 +1457,16 @@ static void shrink_worker(struct work_struct *w)
 				break;
 
 			attempts = 0;
-			goto resched;
+			continue;
 		}
 
-		ret = shrink_memcg(memcg, NR_ZSWAP_WB_BATCH);
+		batch_size = min(upper_pages - lower_pages, NR_ZSWAP_WB_BATCH);
+		shrunk = shrink_memcg(iter_memcg, batch_size);
 		/* drop the extra reference */
-		mem_cgroup_put(memcg);
+		mem_cgroup_put(iter_memcg);
+
+		/* zswap total pages might have changed, refresh it. */
+		upper_pages = zswap_total_pages();
 
 		/*
 		 * There are no writeback-candidate pages in the memcg.
@@ -1456,15 +1474,23 @@ static void shrink_worker(struct work_struct *w)
 		 * with pages in zswap. Skip this without incrementing attempts
 		 * and failures.
 		 */
-		if (ret == -ENOENT)
+		if (shrunk == -ENOENT)
 			continue;
 		++attempts;
 
-		if (ret <= 0 && ++failures == MAX_RECLAIM_RETRIES)
+		if (shrunk <= 0 && ++failures == MAX_RECLAIM_RETRIES)
 			break;
-resched:
-		cond_resched();
-	} while (zswap_total_pages() > thr);
+	}
+}
+
+static void shrink_worker(struct work_struct *w)
+{
+	unsigned long thr;
+
+	/* Reclaim down to the accept threshold */
+	thr = zswap_accept_thr_pages();
+
+	zswap_try_to_writeback(thr, zswap_total_pages());
 }
 
 /*********************************
Signature · 1 line
2.34.1
maintainer
#9 lore raw

Re: [PATCH v4 2/5] mm/zswap: Factor writeback loop out of shrink_worker()

Quoted 11 lines

+/*
+ * Walk the memcg tree and write back zswap pages until the
+ * (lower_pages, upper_pages) window closes, or abort encounter
+ * MAX_RECLAIM_RETRIES times of the following conditions:
+ * - No writeback-candidate memcgs found in a memcg tree walk.
+ * - Shrinking a writeback-candidate memcg failed.
+ *
+ * For shrink_worker(), it passes lower=thr and upper=zswap_total_pages().
+ * The @upper limit is refreshed in each iteration by re-evaluating
+ * zswap_total_pages(), and the window closes once the total falls
+ * below the threshold.

This is the wrong abstraction level, and it's obvious by the fact that the function calls zswap_total_pages() again to recalcualte 'upper_pages'. It gets much worse in the next patch as well.

The lower_pages and upper_pages thing is also unnecessarily hard to follow.

The core of the reuse here is the retry logic. So maybe keep the memcg iteration in the callers, and define a function that takes in one memcg and reclaims one batch from it? failures and attempts can be passed into the function to maintain the state across scans of different memcgs, like zswap_shrink_walk_arg?

WDYT?

Quoted 71 lines

+ */

mm/zswap.c +33 -9
+static void zswap_try_to_writeback(unsigned long lower_pages,
+				   unsigned long upper_pages)
+{
+	int failures = 0, attempts = 0;
+	struct mem_cgroup *iter_memcg;
+
+	while (lower_pages < upper_pages) {
+		unsigned long batch_size;
+		long shrunk;
 
-		if (!memcg) {
+		cond_resched();
+
+		iter_memcg = zswap_iter_global();
+		if (!iter_memcg) {
 			/*
 			 * Continue shrinking without incrementing failures if
 			 * we found candidate memcgs in the last tree walk.
@@ -1443,12 +1457,16 @@ static void shrink_worker(struct work_struct *w)
 				break;
 
 			attempts = 0;
-			goto resched;
+			continue;
 		}
 
-		ret = shrink_memcg(memcg, NR_ZSWAP_WB_BATCH);
+		batch_size = min(upper_pages - lower_pages, NR_ZSWAP_WB_BATCH);
+		shrunk = shrink_memcg(iter_memcg, batch_size);
 		/* drop the extra reference */
-		mem_cgroup_put(memcg);
+		mem_cgroup_put(iter_memcg);
+
+		/* zswap total pages might have changed, refresh it. */
+		upper_pages = zswap_total_pages();
 
 		/*
 		 * There are no writeback-candidate pages in the memcg.
@@ -1456,15 +1474,23 @@ static void shrink_worker(struct work_struct *w)
 		 * with pages in zswap. Skip this without incrementing attempts
 		 * and failures.
 		 */
-		if (ret == -ENOENT)
+		if (shrunk == -ENOENT)
 			continue;
 		++attempts;
 
-		if (ret <= 0 && ++failures == MAX_RECLAIM_RETRIES)
+		if (shrunk <= 0 && ++failures == MAX_RECLAIM_RETRIES)
 			break;
-resched:
-		cond_resched();
-	} while (zswap_total_pages() > thr);
+	}
+}
+
+static void shrink_worker(struct work_struct *w)
+{
+	unsigned long thr;
+
+	/* Reclaim down to the accept threshold */
+	thr = zswap_accept_thr_pages();
+
+	zswap_try_to_writeback(thr, zswap_total_pages());
 }
 
 /*********************************
Signature · 1 line
2.34.1
author
#10 lore raw

Re: [PATCH v4 2/5] mm/zswap: Factor writeback loop out of shrink_worker()

Quoted 19 lines · Yosry Ahmed
Quoted 11 lines

+/*
+ * Walk the memcg tree and write back zswap pages until the
+ * (lower_pages, upper_pages) window closes, or abort encounter
+ * MAX_RECLAIM_RETRIES times of the following conditions:
+ * - No writeback-candidate memcgs found in a memcg tree walk.
+ * - Shrinking a writeback-candidate memcg failed.
+ *
+ * For shrink_worker(), it passes lower=thr and upper=zswap_total_pages().
+ * The @upper limit is refreshed in each iteration by re-evaluating
+ * zswap_total_pages(), and the window closes once the total falls
+ * below the threshold.

This is the wrong abstraction level, and it's obvious by the fact that the function calls zswap_total_pages() again to recalcualte 'upper_pages'. It gets much worse in the next patch as well.

The lower_pages and upper_pages thing is also unnecessarily hard to follow.

Quoted 7 lines

The core of the reuse here is the retry logic. So maybe keep the memcg iteration in the callers, and define a function that takes in one memcg and reclaims one batch from it? failures and attempts can be passed into the function to maintain the state across scans of different memcgs, like zswap_shrink_walk_arg?

WDYT?

Perhaps something like this?

struct zswap_shrink_state {
    int attempts;
    int failures;
    bool stop;
};

static bool zswap_shrink_no_candidate(struct zswap_shrink_state *s)

{
    if (!s->attempts && ++s->failures == MAX_RECLAIM_RETRIES)
        return true;

    s->attempts = 0;
    return false;
}

static long zswap_shrink_one(struct mem_cgroup *memcg,
struct zswap_shrink_state *s)

{
    long shrunk;

    shrunk = shrink_memcg(memcg, NR_ZSWAP_WB_BATCH);
    if (shrunk == -ENOENT)
        return 0;

    s->attempts++;
    if (shrunk <= 0 && ++s->failures == MAX_RECLAIM_RETRIES)
        s->stop = true;

    return shrunk;
}

static void shrink_worker(struct work_struct *w)

{
    struct zswap_shrink_state s = {};
    unsigned long thr;

    /* Reclaim down to the accept threshold */
    thr = zswap_accept_thr_pages();

    while (zswap_total_pages() > thr) {
        struct mem_cgroup *memcg;

        cond_resched();

        memcg = zswap_iter_global();
        if (!memcg) {
            if (zswap_shrink_no_candidate(&s))
                break;
            continue;
        }

        zswap_shrink_one(memcg, &s);
        /* Drop the extra reference taken by the iterator. */
        mem_cgroup_put(memcg);
        if (s.stop)
            break;
    }
}

We could also fold the logic of zswap_shrink_no_candidate() into zswap_shrink_one(), but adding a !memcg check inside zswap_shrink_one() feels a bit awkward.

WDYT?

Thanks,
Hao

maintainer
#11 lore raw

Re: [PATCH v4 2/5] mm/zswap: Factor writeback loop out of shrink_worker()

Quoted 51 lines · Hao Jia
Quoted 19 lines · Yosry Ahmed
Quoted 11 lines

+/*
+ * Walk the memcg tree and write back zswap pages until the
+ * (lower_pages, upper_pages) window closes, or abort encounter
+ * MAX_RECLAIM_RETRIES times of the following conditions:
+ * - No writeback-candidate memcgs found in a memcg tree walk.
+ * - Shrinking a writeback-candidate memcg failed.
+ *
+ * For shrink_worker(), it passes lower=thr and upper=zswap_total_pages().
+ * The @upper limit is refreshed in each iteration by re-evaluating
+ * zswap_total_pages(), and the window closes once the total falls
+ * below the threshold.

This is the wrong abstraction level, and it's obvious by the fact that the function calls zswap_total_pages() again to recalcualte 'upper_pages'. It gets much worse in the next patch as well.

The lower_pages and upper_pages thing is also unnecessarily hard to follow.

Quoted 7 lines

The core of the reuse here is the retry logic. So maybe keep the memcg iteration in the callers, and define a function that takes in one memcg and reclaims one batch from it? failures and attempts can be passed into the function to maintain the state across scans of different memcgs, like zswap_shrink_walk_arg?

WDYT?

Perhaps something like this?

struct zswap_shrink_state {
     int attempts;
     int failures;
     bool stop;
};

static bool zswap_shrink_no_candidate(struct zswap_shrink_state *s)

{
     if (!s->attempts && ++s->failures == MAX_RECLAIM_RETRIES)
         return true;

     s->attempts = 0;
     return false;
}

static long zswap_shrink_one(struct mem_cgroup *memcg,
struct zswap_shrink_state *s)

Quoted 10 lines
{
     long shrunk;

     shrunk = shrink_memcg(memcg, NR_ZSWAP_WB_BATCH);
     if (shrunk == -ENOENT)
         return 0;

     s->attempts++;
     if (shrunk <= 0 && ++s->failures == MAX_RECLAIM_RETRIES)
         s->stop = true;

Do we need 'stop' or can we just return a value here to indicate that we should stop (e.g. -EBUSY)?

Quoted 32 lines
     return shrunk;
}

static void shrink_worker(struct work_struct *w)

{
     struct zswap_shrink_state s = {};
     unsigned long thr;

     /* Reclaim down to the accept threshold */
     thr = zswap_accept_thr_pages();

     while (zswap_total_pages() > thr) {
         struct mem_cgroup *memcg;

         cond_resched();

         memcg = zswap_iter_global();
         if (!memcg) {
             if (zswap_shrink_no_candidate(&s))
                 break;
             continue;
         }

         zswap_shrink_one(memcg, &s);
         /* Drop the extra reference taken by the iterator. */
         mem_cgroup_put(memcg);
         if (s.stop)
             break;
     }
}
Quoted 5 lines

We could also fold the logic of zswap_shrink_no_candidate() into zswap_shrink_one(), but adding a !memcg check inside zswap_shrink_one() feels a bit awkward.

WDYT?

I think splitting the shrink/retry logic over 2 functions makes it more difficult to follow, so yeah I think fold zswap_shrink_no_candidate() into zswap_shrink_one(). Then the callers only need to iterate memcgs (depending on the context) and call zswap_shrink_one() for each of them.

author
#12 lore raw

Re: [PATCH v4 2/5] mm/zswap: Factor writeback loop out of shrink_worker()

Quoted 34 lines · Yosry Ahmed
Quoted 33 lines · Hao Jia

On 2026/6/23 07:36, Yosry Ahmed wrote:

Perhaps something like this?

struct zswap_shrink_state {
      int attempts;
      int failures;
      bool stop;
};

static bool zswap_shrink_no_candidate(struct zswap_shrink_state *s)

{
      if (!s->attempts && ++s->failures == MAX_RECLAIM_RETRIES)
          return true;

      s->attempts = 0;
      return false;
}

static long zswap_shrink_one(struct mem_cgroup *memcg,
struct zswap_shrink_state *s)

{
      long shrunk;

      shrunk = shrink_memcg(memcg, NR_ZSWAP_WB_BATCH);
      if (shrunk == -ENOENT)
          return 0;
Quoted 7 lines
Quoted 3 lines
      s->attempts++;
      if (shrunk <= 0 && ++s->failures == MAX_RECLAIM_RETRIES)
          s->stop = true;

Do we need 'stop' or can we just return a value here to indicate that we should stop (e.g. -EBUSY)?

Perhaps we could return -EAGAIN instead of -EBUSY? This would align with the semantics of the memory.reclaim interface, which returns -EAGAIN when it reclaims fewer bytes than requested.

Quoted 33 lines
Quoted 24 lines
      return shrunk;
}

static void shrink_worker(struct work_struct *w)

{
      struct zswap_shrink_state s = {};
      unsigned long thr;

      /* Reclaim down to the accept threshold */
      thr = zswap_accept_thr_pages();

      while (zswap_total_pages() > thr) {
          struct mem_cgroup *memcg;

          cond_resched();

          memcg = zswap_iter_global();
          if (!memcg) {
              if (zswap_shrink_no_candidate(&s))
                  break;
              continue;
          }
Quoted 7 lines
          zswap_shrink_one(memcg, &s);
          /* Drop the extra reference taken by the iterator. */
          mem_cgroup_put(memcg);
          if (s.stop)
              break;
      }
}
Quoted 5 lines

I think splitting the shrink/retry logic over 2 functions makes it more difficult to follow, so yeah I think fold zswap_shrink_no_candidate() into zswap_shrink_one(). Then the callers only need to iterate memcgs (depending on the context) and call zswap_shrink_one() for each of them.

So, something like this?

/* Track progress of a memcg-tree writeback walk. */

struct zswap_shrink_state {
    int attempts;
    int failures;
};

/*
* Take one step of a memcg-tree writeback walk driven by the caller's
* iterator, and fold the result into @s, the retry bookkeeping shared
* across steps. @memcg is the iterator's current memcg, or NULL once
* it has wrapped around after a full pass over the tree.
*
* The function returns -EAGAIN to signal the caller to abort the walk
* after encountering the following conditions MAX_RECLAIM_RETRIES times:
* - No writeback-candidate memcgs were found in a memcg tree walk.
* - Shrinking a writeback-candidate memcg failed.
*
* Return: The number of compressed bytes written back (>= 0), or -EAGAIN
* once the retry budget is exhausted and the caller should abort the walk. */
static long zswap_shrink_one(struct mem_cgroup *memcg,
struct zswap_shrink_state *s)

{
    long shrunk;

    /*
     * If the iterator has completed a full pass, update the shrink state
     * and check whether we should keep going.
     */
    if (!memcg) {
        /*
         * Continue shrinking without incrementing failures if we found
         * candidate memcgs in the last tree walk.
         */
        if (!s->attempts && ++s->failures == MAX_RECLAIM_RETRIES)
            return -EAGAIN;
        s->attempts = 0;
        return 0;
    }

    shrunk = shrink_memcg(memcg, NR_ZSWAP_WB_BATCH);

    /*
     * There are no writeback-candidate pages in the memcg. This is not an
     * issue as long as we can find another memcg with pages in zswap. Skip
     * this without incrementing attempts and failures.
     */
    if (shrunk == -ENOENT)
        return 0;
    s->attempts++;

    if (shrunk <= 0 && ++s->failures == MAX_RECLAIM_RETRIES)
        return -EAGAIN;

    return shrunk;
}

static void shrink_worker(struct work_struct *w)

{
    struct zswap_shrink_state s = {};
    unsigned long thr;

    /* Reclaim down to the accept threshold */
    thr = zswap_accept_thr_pages();

    while (zswap_total_pages() > thr) {
        struct mem_cgroup *memcg;
        long ret;

        cond_resched();

        memcg = zswap_iter_global();
        ret = zswap_shrink_one(memcg, &s);
        /* drop the extra reference taken by zswap_iter_global() */
        mem_cgroup_put(memcg);
        if (ret == -EAGAIN)
            break;
    }
}
maintainer
#13 lore raw

Re: [PATCH v4 2/5] mm/zswap: Factor writeback loop out of shrink_worker()

Quoted 13 lines
Quoted 13 lines
Quoted 12 lines

static long zswap_shrink_one(struct mem_cgroup *memcg,
struct zswap_shrink_state *s)

{
      long shrunk;

      shrunk = shrink_memcg(memcg, NR_ZSWAP_WB_BATCH);
      if (shrunk == -ENOENT)
          return 0;

      s->attempts++;
      if (shrunk <= 0 && ++s->failures == MAX_RECLAIM_RETRIES)
          s->stop = true;
Quoted 7 lines
Quoted 3 lines

Do we need 'stop' or can we just return a value here to indicate that we should stop (e.g. -EBUSY)?

Perhaps we could return -EAGAIN instead of -EBUSY? This would align with the semantics of the memory.reclaim interface, which returns -EAGAIN when it reclaims fewer bytes than requested.

Hmm but -EAGAIN tells the caller to try again, while here -EAGAIN tells the caller *not* to try again because we exhausted all retries?

Quoted 8 lines
Quoted 6 lines

I think splitting the shrink/retry logic over 2 functions makes it more difficult to follow, so yeah I think fold zswap_shrink_no_candidate() into zswap_shrink_one(). Then the callers only need to iterate memcgs (depending on the context) and call zswap_shrink_one() for each of them.

So, something like this?

Yeah, something like this :)

Quoted 3 lines

/* Track progress of a memcg-tree writeback walk. */

struct zswap_shrink_state {
     int attempts;

While at it, I think "attempts" is really the number of scans, right? Should we rename it? Maybe "scans" or similar?

Quoted 13 lines
     int failures;
};

/*
* Take one step of a memcg-tree writeback walk driven by the caller's
* iterator, and fold the result into @s, the retry bookkeeping shared
* across steps. @memcg is the iterator's current memcg, or NULL once
* it has wrapped around after a full pass over the tree.
*
* The function returns -EAGAIN to signal the caller to abort the walk
* after encountering the following conditions MAX_RECLAIM_RETRIES times:
* - No writeback-candidate memcgs were found in a memcg tree walk.
* - Shrinking a writeback-candidate memcg failed.

Orthogonal to this patch, but I wonder if this can be simplified. I wonder if these two conditions can be replaced with "shrinking a memcg that has zswap entries failed". The "no writeback-candidate memcgs in the tree" case seems like we should abort right away instead of retrying?

Nhat, WDYT?

Quoted 5 lines

*
* Return: The number of compressed bytes written back (>= 0), or -EAGAIN
* once the retry budget is exhausted and the caller should abort the walk. */
static long zswap_shrink_one(struct mem_cgroup *memcg,

Nit: zswap_shrink_one_memcg()

BTW, the existing writeback logic has been broken for a while now when memcg is disabled. I think we constantly hit the !memcg case and run out of retries. Not sure if your patch changes this in any way, or if you want to fix that while you're at it :)

Quoted 37 lines

struct zswap_shrink_state *s)

{
     long shrunk;

     /*
      * If the iterator has completed a full pass, update the shrink state
      * and check whether we should keep going.
      */
     if (!memcg) {
         /*
          * Continue shrinking without incrementing failures if we found
          * candidate memcgs in the last tree walk.
          */
         if (!s->attempts && ++s->failures == MAX_RECLAIM_RETRIES)
             return -EAGAIN;
         s->attempts = 0;
         return 0;
     }

     shrunk = shrink_memcg(memcg, NR_ZSWAP_WB_BATCH);

     /*
      * There are no writeback-candidate pages in the memcg. This is not an
      * issue as long as we can find another memcg with pages in zswap. Skip
      * this without incrementing attempts and failures.
      */
     if (shrunk == -ENOENT)
         return 0;
     s->attempts++;

     if (shrunk <= 0 && ++s->failures == MAX_RECLAIM_RETRIES)
         return -EAGAIN;

     return shrunk;
}

static void shrink_worker(struct work_struct *w)

Quoted 14 lines
{
     struct zswap_shrink_state s = {};
     unsigned long thr;

     /* Reclaim down to the accept threshold */
     thr = zswap_accept_thr_pages();

     while (zswap_total_pages() > thr) {
         struct mem_cgroup *memcg;
         long ret;

         cond_resched();

         memcg = zswap_iter_global();

Do we still need this helper? Or should we just keep the memcg iteration open-coded?

Quoted 7 lines
         ret = zswap_shrink_one(memcg, &s);
         /* drop the extra reference taken by zswap_iter_global() */
         mem_cgroup_put(memcg);
         if (ret == -EAGAIN)
             break;
     }
}
author
#14 lore raw

Re: [PATCH v4 2/5] mm/zswap: Factor writeback loop out of shrink_worker()

Quoted 17 lines · Yosry Ahmed
Quoted 17 lines
Quoted 16 lines
Quoted 12 lines

static long zswap_shrink_one(struct mem_cgroup *memcg,
struct zswap_shrink_state *s)

{
       long shrunk;

       shrunk = shrink_memcg(memcg, NR_ZSWAP_WB_BATCH);
       if (shrunk == -ENOENT)
           return 0;

       s->attempts++;
       if (shrunk <= 0 && ++s->failures == MAX_RECLAIM_RETRIES)
           s->stop = true;

Do we need 'stop' or can we just return a value here to indicate that we should stop (e.g. -EBUSY)?

Quoted 7 lines
Quoted 3 lines

Perhaps we could return -EAGAIN instead of -EBUSY? This would align with the semantics of the memory.reclaim interface, which returns -EAGAIN when it reclaims fewer bytes than requested.

Hmm but -EAGAIN tells the caller to try again, while here -EAGAIN tells the caller *not* to try again because we exhausted all retries?

Okay, let's go with -EBUSY.

Quoted 32 lines
Quoted 8 lines
Quoted 6 lines

I think splitting the shrink/retry logic over 2 functions makes it more difficult to follow, so yeah I think fold zswap_shrink_no_candidate() into zswap_shrink_one(). Then the callers only need to iterate memcgs (depending on the context) and call zswap_shrink_one() for each of them.

So, something like this?

Yeah, something like this :)

Quoted 3 lines

/* Track progress of a memcg-tree writeback walk. */

struct zswap_shrink_state {
      int attempts;

While at it, I think "attempts" is really the number of scans, right? Should we rename it? Maybe "scans" or similar?

Quoted 13 lines
      int failures;
};

/*
* Take one step of a memcg-tree writeback walk driven by the caller's
* iterator, and fold the result into @s, the retry bookkeeping shared
* across steps. @memcg is the iterator's current memcg, or NULL once
* it has wrapped around after a full pass over the tree.
*
* The function returns -EAGAIN to signal the caller to abort the walk
* after encountering the following conditions MAX_RECLAIM_RETRIES times:
* - No writeback-candidate memcgs were found in a memcg tree walk.
* - Shrinking a writeback-candidate memcg failed.

Quoted 8 lines

Orthogonal to this patch, but I wonder if this can be simplified. I wonder if these two conditions can be replaced with "shrinking a memcg that has zswap entries failed". The "no writeback-candidate memcgs in the tree" case seems like we should abort right away instead of retrying?

Nhat, WDYT?

Perhaps something like the following is what you had in mind? I've drafted the implementation below to make it easier for Nhat to compare with the previous behavior.

Quoted 6 lines
Quoted 5 lines

*
* Return: The number of compressed bytes written back (>= 0), or -EAGAIN
* once the retry budget is exhausted and the caller should abort the walk. */
static long zswap_shrink_one(struct mem_cgroup *memcg,

Quoted 6 lines

Nit: zswap_shrink_one_memcg()

BTW, the existing writeback logic has been broken for a while now when memcg is disabled. I think we constantly hit the !memcg case and run out of retries. Not sure if your patch changes this in any way, or if you want to fix that while you're at it :)

Yes, I'd be happy to do that. However, would it be better to submit a separate fix patch or combine it with this one?

Quoted 49 lines
Quoted 49 lines

struct zswap_shrink_state *s)

{
      long shrunk;

      /*
       * If the iterator has completed a full pass, update the shrink state
       * and check whether we should keep going.
       */
      if (!memcg) {
          /*
           * Continue shrinking without incrementing failures if we found
           * candidate memcgs in the last tree walk.
           */
          if (!s->attempts && ++s->failures == MAX_RECLAIM_RETRIES)
              return -EAGAIN;
          s->attempts = 0;
          return 0;
      }

      shrunk = shrink_memcg(memcg, NR_ZSWAP_WB_BATCH);

      /*
       * There are no writeback-candidate pages in the memcg. This is not an
       * issue as long as we can find another memcg with pages in zswap. Skip
       * this without incrementing attempts and failures.
       */
      if (shrunk == -ENOENT)
          return 0;
      s->attempts++;

      if (shrunk <= 0 && ++s->failures == MAX_RECLAIM_RETRIES)
          return -EAGAIN;

      return shrunk;
}

static void shrink_worker(struct work_struct *w)

{
      struct zswap_shrink_state s = {};
      unsigned long thr;

      /* Reclaim down to the accept threshold */
      thr = zswap_accept_thr_pages();

      while (zswap_total_pages() > thr) {
          struct mem_cgroup *memcg;
          long ret;
Quoted 6 lines
Quoted 3 lines
          cond_resched();

          memcg = zswap_iter_global();

Do we still need this helper? Or should we just keep the memcg iteration open-coded?

Done.

Quoted 8 lines
Quoted 8 lines
          ret = zswap_shrink_one(memcg, &s);
          /* drop the extra reference taken by zswap_iter_global() */
          mem_cgroup_put(memcg);
          if (ret == -EAGAIN)
              break;
      }
}

/* Track progress of a memcg-tree writeback walk. */

struct zswap_shrink_state {
    int scans;
    int failures;
};

/*
* Take one step of a memcg-tree writeback walk driven by the caller's
* iterator, and fold the result into @s, the retry bookkeeping shared
* across steps. @memcg is the iterator's current memcg, or NULL once
* it has wrapped around after a full pass over the tree.
*
* The function returns -EBUSY to signal the caller to abort the walk when
* either of the following occurs:
* - A full pass over the tree found no writeback-candidate memcg.
* - Shrinking a writeback-candidate memcg failed MAX_RECLAIM_RETRIES
times.
*
* When memory cgroup is disabled, the iterator always yields NULL. All
* zswap entries then live on the root list_lru, so NULL is treated as the
* root memcg and shrunk directly rather than as a completed tree pass. *
* Return: The number of compressed bytes written back (>= 0), or -EBUSY
* when the caller should abort the walk.
*/
static long zswap_shrink_one_memcg(struct mem_cgroup *memcg,
struct zswap_shrink_state *s)

{
    bool disabled = mem_cgroup_disabled();
    long shrunk;

    /*
     * If the iterator has completed a full pass, update the shrink state
     * and check whether we should keep going.
     * With memcg disabled the iterator always yields NULL, so fall through
     * and shrink the root memcg directly instead.
     */
    if (!memcg && !disabled) {
        /*
         * Abort if no writeback-candidate memcgs in the last tree walk.
         * Otherwise reset the scans count and continue.
         */
        if (!s->scans)
            return -EBUSY;
        s->scans = 0;
        return 0;
    }

    shrunk = shrink_memcg(memcg, NR_ZSWAP_WB_BATCH);

    /*
     * There are no writeback-candidate pages in the memcg. With memcg
     * enabled this is not an issue as long as we can find another memcg
     * with pages in zswap, so skip without counting it as a candidate.
     * With memcg disabled the root LRU is the only target, so we should
     * abort if it has no writeback-candidate pages.
     */
    if (shrunk == -ENOENT)
        return disabled ? -EBUSY : 0;
    s->scans++;

    if (shrunk <= 0 && ++s->failures == MAX_RECLAIM_RETRIES)
        return -EBUSY;

    return shrunk;
}

static void shrink_worker(struct work_struct *w)

{
    struct zswap_shrink_state s = {};
    unsigned long thr;

    /* Reclaim down to the accept threshold */
    thr = zswap_accept_thr_pages();

    /*
     * Global reclaim will select cgroup in a round-robin fashion from all
     * online memcgs, but memcgs that have no pages in zswap and
     * writeback-disabled memcgs (memory.zswap.writeback=0) are not
     * candidates for shrinking.
     *
     * We save iteration cursor memcg into zswap_next_shrink,
     * which can be modified by the offline memcg cleaner
     * zswap_memcg_offline_cleanup().
     *
     * Since the offline cleaner is called only once, we cannot leave an
     * offline memcg reference in zswap_next_shrink.
     * We can rely on the cleaner only if we get online memcg under lock.
     *
     * If we get an offline memcg, we cannot determine if the cleaner has
     * already been called or will be called later. We must put back the
     * reference before returning from this function. Otherwise, the
     * offline memcg left in zswap_next_shrink will hold the reference
     * until the next run of shrink_worker().
     */
    while (zswap_total_pages() > thr) {
        struct mem_cgroup *memcg;
        long ret;

        cond_resched();
        /*
         * Start shrinking from the next memcg after zswap_next_shrink.
         * When the offline cleaner has already advanced the cursor,
         * advancing the cursor here overlooks one memcg, but this
         * should be negligibly rare.
         *
         * If we get an online memcg, keep the extra reference in case
         * the original one obtained by mem_cgroup_iter() is dropped by
         * zswap_memcg_offline_cleanup() while we are shrinking the
         * memcg.
         */
        spin_lock(&zswap_shrink_lock);
        do {
            memcg = mem_cgroup_iter(NULL, zswap_next_shrink, NULL);
            zswap_next_shrink = memcg;
        } while (memcg && !mem_cgroup_tryget_online(memcg));
        spin_unlock(&zswap_shrink_lock);

        ret = zswap_shrink_one_memcg(memcg, &s);
        /* drop the extra reference taken above */
        mem_cgroup_put(memcg);
        if (ret == -EBUSY)
            break;
    }
}

Thanks,
Hao

maintainer
#15 lore raw

Re: [PATCH v4 2/5] mm/zswap: Factor writeback loop out of shrink_worker()

Quoted 17 lines
Quoted 17 lines
Quoted 10 lines

/*
* Take one step of a memcg-tree writeback walk driven by the caller's
* iterator, and fold the result into @s, the retry bookkeeping shared
* across steps. @memcg is the iterator's current memcg, or NULL once
* it has wrapped around after a full pass over the tree.
*
* The function returns -EAGAIN to signal the caller to abort the walk
* after encountering the following conditions MAX_RECLAIM_RETRIES times:
* - No writeback-candidate memcgs were found in a memcg tree walk.
* - Shrinking a writeback-candidate memcg failed.

Orthogonal to this patch, but I wonder if this can be simplified. I wonder if these two conditions can be replaced with "shrinking a memcg that has zswap entries failed". The "no writeback-candidate memcgs in the tree" case seems like we should abort right away instead of retrying?

Quoted 6 lines
Quoted 2 lines

Nhat, WDYT?

Perhaps something like the following is what you had in mind? I've drafted the implementation below to make it easier for Nhat to compare with the previous behavior.

Hmm I think if we pursue this it should be in a separate patch or even outside of this series, ideally with numbers/proof that it's not introducing regressions to the scenario that lead to its introduction.

Quoted 10 lines
Quoted 10 lines
Quoted 7 lines

*
* Return: The number of compressed bytes written back (>= 0), or -EAGAIN
* once the retry budget is exhausted and the caller should abort the walk. */
static long zswap_shrink_one(struct mem_cgroup *memcg,

Nit: zswap_shrink_one_memcg()

Quoted 7 lines
Quoted 4 lines

BTW, the existing writeback logic has been broken for a while now when memcg is disabled. I think we constantly hit the !memcg case and run out of retries. Not sure if your patch changes this in any way, or if you want to fix that while you're at it :)

Yes, I'd be happy to do that. However, would it be better to submit a separate fix patch or combine it with this one?

A separate patch. Feel free to send it with this series to avoid conflicts, but probably as patch 1 as we'll want to CC stable on it.

[..]

Quoted 6 lines

/* Track progress of a memcg-tree writeback walk. */

struct zswap_shrink_state {
     int scans;
     int failures;
};
Quoted 15 lines

/*
* Take one step of a memcg-tree writeback walk driven by the caller's
* iterator, and fold the result into @s, the retry bookkeeping shared
* across steps. @memcg is the iterator's current memcg, or NULL once
* it has wrapped around after a full pass over the tree.
*
* The function returns -EBUSY to signal the caller to abort the walk when
* either of the following occurs:
* - A full pass over the tree found no writeback-candidate memcg.
* - Shrinking a writeback-candidate memcg failed MAX_RECLAIM_RETRIES
times.
*
* When memory cgroup is disabled, the iterator always yields NULL. All
* zswap entries then live on the root list_lru, so NULL is treated as the
* root memcg and shrunk directly rather than as a completed tree pass.

I think this chunk should be moved above the code returning -EBUSY when mem_cgroup_disabled() is true, and probably made more succinct as it should be obvious.

Quoted 8 lines

*
* Return: The number of compressed bytes written back (>= 0), or -EBUSY
* when the caller should abort the walk.
*/
static long zswap_shrink_one_memcg(struct mem_cgroup *memcg,
struct zswap_shrink_state *s)

{
     bool disabled = mem_cgroup_disabled();

No need to store this in a variable AFAICT, it's a static branch and it's clearer to just call it directly in both call sites imo.

Quoted 100 lines
     long shrunk;

     /*
      * If the iterator has completed a full pass, update the shrink state
      * and check whether we should keep going.
      * With memcg disabled the iterator always yields NULL, so fall through
      * and shrink the root memcg directly instead.
      */
     if (!memcg && !disabled) {
         /*
          * Abort if no writeback-candidate memcgs in the last tree walk.
          * Otherwise reset the scans count and continue.
          */
         if (!s->scans)
             return -EBUSY;
         s->scans = 0;
         return 0;
     }

     shrunk = shrink_memcg(memcg, NR_ZSWAP_WB_BATCH);

     /*
      * There are no writeback-candidate pages in the memcg. With memcg
      * enabled this is not an issue as long as we can find another memcg
      * with pages in zswap, so skip without counting it as a candidate.
      * With memcg disabled the root LRU is the only target, so we should
      * abort if it has no writeback-candidate pages.
      */
     if (shrunk == -ENOENT)
         return disabled ? -EBUSY : 0;
     s->scans++;

     if (shrunk <= 0 && ++s->failures == MAX_RECLAIM_RETRIES)
         return -EBUSY;

     return shrunk;
}

static void shrink_worker(struct work_struct *w)

{
     struct zswap_shrink_state s = {};
     unsigned long thr;

     /* Reclaim down to the accept threshold */
     thr = zswap_accept_thr_pages();

     /*
      * Global reclaim will select cgroup in a round-robin fashion from all
      * online memcgs, but memcgs that have no pages in zswap and
      * writeback-disabled memcgs (memory.zswap.writeback=0) are not
      * candidates for shrinking.
      *
      * We save iteration cursor memcg into zswap_next_shrink,
      * which can be modified by the offline memcg cleaner
      * zswap_memcg_offline_cleanup().
      *
      * Since the offline cleaner is called only once, we cannot leave an
      * offline memcg reference in zswap_next_shrink.
      * We can rely on the cleaner only if we get online memcg under lock.
      *
      * If we get an offline memcg, we cannot determine if the cleaner has
      * already been called or will be called later. We must put back the
      * reference before returning from this function. Otherwise, the
      * offline memcg left in zswap_next_shrink will hold the reference
      * until the next run of shrink_worker().
      */
     while (zswap_total_pages() > thr) {
         struct mem_cgroup *memcg;
         long ret;

         cond_resched();
         /*
          * Start shrinking from the next memcg after zswap_next_shrink.
          * When the offline cleaner has already advanced the cursor,
          * advancing the cursor here overlooks one memcg, but this
          * should be negligibly rare.
          *
          * If we get an online memcg, keep the extra reference in case
          * the original one obtained by mem_cgroup_iter() is dropped by
          * zswap_memcg_offline_cleanup() while we are shrinking the
          * memcg.
          */
         spin_lock(&zswap_shrink_lock);
         do {
             memcg = mem_cgroup_iter(NULL, zswap_next_shrink, NULL);
             zswap_next_shrink = memcg;
         } while (memcg && !mem_cgroup_tryget_online(memcg));
         spin_unlock(&zswap_shrink_lock);

         ret = zswap_shrink_one_memcg(memcg, &s);
         /* drop the extra reference taken above */
         mem_cgroup_put(memcg);
         if (ret == -EBUSY)
             break;
     }
}

Thanks,
Hao

author
#16 lore raw

Re: [PATCH v4 2/5] mm/zswap: Factor writeback loop out of shrink_worker()

Quoted 20 lines · Yosry Ahmed
Quoted 20 lines
Quoted 19 lines
Quoted 10 lines

/*
* Take one step of a memcg-tree writeback walk driven by the caller's
* iterator, and fold the result into @s, the retry bookkeeping shared
* across steps. @memcg is the iterator's current memcg, or NULL once
* it has wrapped around after a full pass over the tree.
*
* The function returns -EAGAIN to signal the caller to abort the walk
* after encountering the following conditions MAX_RECLAIM_RETRIES times:
* - No writeback-candidate memcgs were found in a memcg tree walk.
* - Shrinking a writeback-candidate memcg failed.

Orthogonal to this patch, but I wonder if this can be simplified. I wonder if these two conditions can be replaced with "shrinking a memcg that has zswap entries failed". The "no writeback-candidate memcgs in the tree" case seems like we should abort right away instead of retrying?

Nhat, WDYT?

Quoted 8 lines
Quoted 3 lines

Perhaps something like the following is what you had in mind? I've drafted the implementation below to make it easier for Nhat to compare with the previous behavior.

Hmm I think if we pursue this it should be in a separate patch or even outside of this series, ideally with numbers/proof that it's not introducing regressions to the scenario that lead to its introduction.

Got it. I will temporarily leave this part out of the current patch series.

Quoted 15 lines
Quoted 15 lines
Quoted 14 lines
Quoted 7 lines

*
* Return: The number of compressed bytes written back (>= 0), or -EAGAIN
* once the retry budget is exhausted and the caller should abort the walk. */
static long zswap_shrink_one(struct mem_cgroup *memcg,

Nit: zswap_shrink_one_memcg()

BTW, the existing writeback logic has been broken for a while now when memcg is disabled. I think we constantly hit the !memcg case and run out of retries. Not sure if your patch changes this in any way, or if you want to fix that while you're at it :)

Quoted 6 lines
Quoted 2 lines

Yes, I'd be happy to do that. However, would it be better to submit a separate fix patch or combine it with this one?

A separate patch. Feel free to send it with this series to avoid conflicts, but probably as patch 1 as we'll want to CC stable on it.

Done, and I've just submitted the v5 patch.

[v5] https://lore.kernel.org/all/[email protected]

Please take a look when you have a chance. Thank you very much for your review!

Quoted 24 lines

[..]

Quoted 6 lines

/* Track progress of a memcg-tree writeback walk. */

struct zswap_shrink_state {
      int scans;
      int failures;
};
Quoted 15 lines

/*
* Take one step of a memcg-tree writeback walk driven by the caller's
* iterator, and fold the result into @s, the retry bookkeeping shared
* across steps. @memcg is the iterator's current memcg, or NULL once
* it has wrapped around after a full pass over the tree.
*
* The function returns -EBUSY to signal the caller to abort the walk when
* either of the following occurs:
* - A full pass over the tree found no writeback-candidate memcg.
* - Shrinking a writeback-candidate memcg failed MAX_RECLAIM_RETRIES
times.
*
* When memory cgroup is disabled, the iterator always yields NULL. All
* zswap entries then live on the root list_lru, so NULL is treated as the
* root memcg and shrunk directly rather than as a completed tree pass.

Quoted 3 lines

I think this chunk should be moved above the code returning -EBUSY when mem_cgroup_disabled() is true, and probably made more succinct as it should be obvious.

Done in v5.

Quoted 10 lines
Quoted 9 lines

*
* Return: The number of compressed bytes written back (>= 0), or -EBUSY
* when the caller should abort the walk.
*/
static long zswap_shrink_one_memcg(struct mem_cgroup *memcg,
struct zswap_shrink_state *s)

{
      bool disabled = mem_cgroup_disabled();
Quoted 2 lines

No need to store this in a variable AFAICT, it's a static branch and it's clearer to just call it directly in both call sites imo.

Done, and I've just submitted the v5 patch.

Thanks,
Hao

Quoted 38 lines
Quoted 38 lines
      long shrunk;

      /*
       * If the iterator has completed a full pass, update the shrink state
       * and check whether we should keep going.
       * With memcg disabled the iterator always yields NULL, so fall through
       * and shrink the root memcg directly instead.
       */
      if (!memcg && !disabled) {
          /*
           * Abort if no writeback-candidate memcgs in the last tree walk.
           * Otherwise reset the scans count and continue.
           */
          if (!s->scans)
              return -EBUSY;
          s->scans = 0;
          return 0;
      }

      shrunk = shrink_memcg(memcg, NR_ZSWAP_WB_BATCH);

      /*
       * There are no writeback-candidate pages in the memcg. With memcg
       * enabled this is not an issue as long as we can find another memcg
       * with pages in zswap, so skip without counting it as a candidate.
       * With memcg disabled the root LRU is the only target, so we should
       * abort if it has no writeback-candidate pages.
       */
      if (shrunk == -ENOENT)
          return disabled ? -EBUSY : 0;
      s->scans++;

      if (shrunk <= 0 && ++s->failures == MAX_RECLAIM_RETRIES)
          return -EBUSY;

      return shrunk;
}
author
#17 lore raw

PATCH v4 3/5 mm/zswap: Implement proactive writeback

In reply to Hao Jia

From: Hao Jia <[email protected]>

Zswap currently writes back pages to backing swap reactively, triggered either by the shrinker or when the pool reaches its size limit. There is no mechanism to control the amount of writeback for a specific memory cgroup. However, users may want to proactively write back zswap pages, e.g., to free up memory for other applications or to prepare for memory-intensive workloads.

Introduce a "zswap_writeback_only" key to the memory.reclaim cgroup interface. When specified, this key bypasses standard memory reclaim and exclusively performs proactive zswap writeback up to the requested budget. If omitted, the default reclaim behavior remains unchanged.

Example usage:
# Write back 10MB of compressed data from zswap to the backing swap echo "10M zswap_writeback_only" > memory.reclaim

Note that the actual amount of compressed data written back may be less than requested due to the zswap second-chance algorithm: referenced entries are rotated on the LRU on the first encounter and only written back on a second pass. If fewer bytes are written back than requested, -EAGAIN is returned, matching the existing memory.reclaim semantics.

Internally, extend user_proactive_reclaim() to parse the new "zswap_writeback_only" token and invoke the dedicated handler zswap_proactive_writeback(). This handler reuses zswap_try_to_writeback() to walk the target memcg subtree, draining per-node zswap LRUs through list_lru_walk_one() with the shrink_memcg_cb() callback.

---
 Documentation/admin-guide/cgroup-v2.rst | 18 ++++-
 Documentation/admin-guide/mm/zswap.rst  | 11 +++-
 include/linux/zswap.h                   |  7 ++
 mm/vmscan.c                             | 14 ++++
 mm/zswap.c                              | 87 +++++++++++++++++++++----
 5 files changed, 120 insertions(+), 17 deletions(-)
Documentation/admin-guide/cgroup-v2.rst +16 -2
index 6efd0095ed99..e52d97e8e9c6 100644
@@ -1425,9 +1425,10 @@ PAGE_SIZE multiple when read back.
 
 The following nested keys are defined.
 
-	  ==========            ================================
+	  ====================  ==================================================
 	  swappiness            Swappiness value to reclaim with
-	  ==========            ================================
+	  zswap_writeback_only  Only perform proactive zswap writeback
+	  ====================  ==================================================
 
 	Specifying a swappiness value instructs the kernel to perform
 	the reclaim with that swappiness value. Note that this has the
@@ -1437,6 +1438,19 @@ The following nested keys are defined.
 	The valid range for swappiness is [0-200, max], setting
 	swappiness=max exclusively reclaims anonymous memory.
 
+	The zswap_writeback_only key skips ordinary memory reclaim and
+	writes back pages from zswap to the backing swap device until
+	the requested amount has been written or no further candidates
+	are found. This is useful to proactively offload cold compressed
+	data from the zswap pool to the swap device. It is only available
+	if zswap writeback is enabled. zswap_writeback_only cannot be
+	combined with swappiness; specifying both returns -EINVAL.
+
+	Example::
+
+	  # Writeback up to 10MB of compressed data from zswap to the backing swap
+	  echo "10M zswap_writeback_only" > memory.reclaim
+
   memory.peak
 	A read-write single value file which exists on non-root cgroups.
 
Documentation/admin-guide/mm/zswap.rst +10 -1
index 2464425c783d..fdeb197d1683 100644
@@ -131,7 +131,16 @@ User can enable it as follows::
   echo Y > /sys/module/zswap/parameters/shrinker_enabled
 
 This can be enabled at the boot time if ``CONFIG_ZSWAP_SHRINKER_DEFAULT_ON`` is
-selected.
+selected. Once enabled, the shrinker automatically writes back zswap pages to
+backing swap during memory reclaim.
+
+If users want to explicitly trigger proactive zswap writeback for a specific
+memory cgroup without invoking standard page reclaim, it can be done as follows::
+
+	echo "10M zswap_writeback_only" > /sys/fs/cgroup/<cgroup-name>/memory.reclaim
+
+Both of the methods mentioned above are subject to the ``memory.zswap.writeback``
+control. This means that ``memory.zswap.writeback`` can prevent all zswap writeback.
 
 A debugfs interface is provided for various statistic about pool size, number
 of pages stored, same-value filled pages and various counters for the reasons
include/linux/zswap.h +7 -0
index 30c193a1207e..7bf38318dab1 100644
@@ -35,6 +35,7 @@ void zswap_lruvec_state_init(struct lruvec *lruvec);
 void zswap_folio_swapin(struct folio *folio);
 bool zswap_is_enabled(void);
 bool zswap_never_enabled(void);
+int zswap_proactive_writeback(struct mem_cgroup *memcg, unsigned long nr_to_writeback);
 #else
 
 struct zswap_lruvec_state {};
@@ -69,6 +70,12 @@ static inline bool zswap_never_enabled(void)
 	return true;
 }
 
+static inline int zswap_proactive_writeback(struct mem_cgroup *memcg,
+					    unsigned long nr_to_writeback)
+{
+	return -EOPNOTSUPP;
+}
+
 #endif
 
 #endif /* _LINUX_ZSWAP_H */
mm/vmscan.c +14 -0
index 299b5d9e8836..2e6c14569fc2 100644
@@ -64,6 +64,7 @@
 
 #include <linux/swapops.h>
 #include <linux/sched/sysctl.h>
+#include <linux/zswap.h>
 
 #include "internal.h"
 #include "swap.h"
@@ -7855,11 +7856,13 @@ static unsigned long __node_reclaim(struct pglist_data *pgdat, gfp_t gfp_mask,
 enum {
 	MEMORY_RECLAIM_SWAPPINESS = 0,
 	MEMORY_RECLAIM_SWAPPINESS_MAX,
+	MEMORY_RECLAIM_ZSWAP_WRITEBACK_ONLY,
 	MEMORY_RECLAIM_NULL,
 };
 static const match_table_t tokens = {
 	{ MEMORY_RECLAIM_SWAPPINESS, "swappiness=%d"},
 	{ MEMORY_RECLAIM_SWAPPINESS_MAX, "swappiness=max"},
+	{ MEMORY_RECLAIM_ZSWAP_WRITEBACK_ONLY, "zswap_writeback_only"},
 	{ MEMORY_RECLAIM_NULL, NULL },
 };
 
@@ -7869,6 +7872,7 @@ int user_proactive_reclaim(char *buf,
 	unsigned int nr_retries = MAX_RECLAIM_RETRIES;
 	unsigned long nr_to_reclaim, nr_reclaimed = 0;
 	int swappiness = -1;
+	bool zswap_writeback_only = false;
 	char *old_buf, *start;
 	substring_t args[MAX_OPT_ARGS];
 	gfp_t gfp_mask = GFP_KERNEL;
@@ -7899,11 +7903,21 @@ int user_proactive_reclaim(char *buf,
 		case MEMORY_RECLAIM_SWAPPINESS_MAX:
 			swappiness = SWAPPINESS_ANON_ONLY;
 			break;
+		case MEMORY_RECLAIM_ZSWAP_WRITEBACK_ONLY:
+			zswap_writeback_only = true;
+			break;
 		default:
 			return -EINVAL;
 		}
 	}
 
+	if (zswap_writeback_only) {
+		/* zswap_writeback_only and swappiness are mutually exclusive. */
+		if (swappiness != -1)
+			return -EINVAL;
+		return zswap_proactive_writeback(memcg, nr_to_reclaim);
+	}
+
 	while (nr_reclaimed < nr_to_reclaim) {
 		/* Will converge on zero, but reclaim enforces a minimum */
 		unsigned long batch_size = (nr_to_reclaim - nr_reclaimed) / 4;
mm/zswap.c +73 -14

139 lines not on this page · raw message

Signature · 1 line
2.34.1
maintainer
#18 lore raw

Re: [PATCH v4 3/5] mm/zswap: Implement proactive writeback

Quoted 24 lines · Hao Jia

From: Hao Jia <[email protected]>

Zswap currently writes back pages to backing swap reactively, triggered either by the shrinker or when the pool reaches its size limit. There is no mechanism to control the amount of writeback for a specific memory cgroup. However, users may want to proactively write back zswap pages, e.g., to free up memory for other applications or to prepare for memory-intensive workloads.

Introduce a "zswap_writeback_only" key to the memory.reclaim cgroup interface. When specified, this key bypasses standard memory reclaim and exclusively performs proactive zswap writeback up to the requested budget. If omitted, the default reclaim behavior remains unchanged.

Example usage:
# Write back 10MB of compressed data from zswap to the backing swap echo "10M zswap_writeback_only" > memory.reclaim

Note that the actual amount of compressed data written back may be less than requested due to the zswap second-chance algorithm: referenced entries are rotated on the LRU on the first encounter and only written back on a second pass. If fewer bytes are written back than requested, -EAGAIN is returned, matching the existing memory.reclaim semantics.

Quoted 6 lines

Internally, extend user_proactive_reclaim() to parse the new "zswap_writeback_only" token and invoke the dedicated handler zswap_proactive_writeback(). This handler reuses zswap_try_to_writeback() to walk the target memcg subtree, draining per-node zswap LRUs through list_lru_walk_one() with the shrink_memcg_cb() callback.

I won't comment on the memcg interface as this is more-or-less a placeholder until an interface is finalized.

Quoted 4 lines

[..]

Quoted 45 lines
mm/zswap.c +26 -4
index e29f8a61412d..28200552dde3 100644
@@ -1423,6 +1423,27 @@ static struct mem_cgroup *zswap_iter_global(void)
 	return memcg;
 }
 
+/*
+ * Local iteration uses a local cursor to select from online memcgs
+ * under @root in a round-robin fashion.
+ *
+ * Pass the previous return value as @prev to advance the round-robin
+ * iteration, or pass NULL to start a new walk. If exiting early before
+ * the iteration completes, the caller must call mem_cgroup_iter_break()
+ * to release the cursor reference.
+ */
+static struct mem_cgroup *zswap_iter_local(struct mem_cgroup *root,
+					   struct mem_cgroup *prev)
+{
+	struct mem_cgroup *memcg;
+
+	do {
+		memcg = mem_cgroup_iter(root, prev, NULL);
+		prev = memcg;
+	} while (memcg && !mem_cgroup_tryget_online(memcg));
+	return memcg;
+}
+
 /*
  * Walk the memcg tree and write back zswap pages until the
  * (lower_pages, upper_pages) window closes, or abort encounter
@@ -1430,16 +1451,23 @@ static struct mem_cgroup *zswap_iter_global(void)
  * - No writeback-candidate memcgs found in a memcg tree walk.
  * - Shrinking a writeback-candidate memcg failed.
  *
- * For shrink_worker(), it passes lower=thr and upper=zswap_total_pages().
- * The @upper limit is refreshed in each iteration by re-evaluating
- * zswap_total_pages(), and the window closes once the total falls
- * below the threshold.
+ * For shrink_worker() (proactive=false), it passes lower=thr and
+ * upper=zswap_total_pages(). The @upper limit is refreshed in each
+ * iteration by re-evaluating zswap_total_pages(), and the window
+ * closes once the total falls below the threshold.
+ *

mm/zswap.c static struct mem_cgroup *zswap_iter_global(void) · line 1466

Quoted 10 lines
mm/zswap.c +7 -2
+ * For zswap_proactive_writeback() (proactive=true), it passes lower=0
+ * and upper=nr_to_writeback. The @lower limit is advanced by the
+ * compressed bytes written back via shrink_memcg(). The window closes
+ * once @nr_to_writeback pages of compressed data have been written back.
  */
-static void zswap_try_to_writeback(unsigned long lower_pages,
-				   unsigned long upper_pages)
+static int zswap_try_to_writeback(struct mem_cgroup *memcg,
+				  unsigned long lower_pages,
+				  unsigned long upper_pages, bool proactive)

As I mentiond in the previous patch, this is the wrong abstraction. The function is extremely tighyl-coupled to the callers, and needing to pass in things like proactive makes it even worse.

It should be limited to reclaiming one batch of pages from a memcg, and the retry logic. Everything else (memcg iteration logic, scan goal checks) should be in the caller.

[..]

Quoted 15 lines
mm/zswap.c +2 -1
 static void shrink_worker(struct work_struct *w)
@@ -1490,7 +1536,7 @@ static void shrink_worker(struct work_struct *w)
 	/* Reclaim down to the accept threshold */
 	thr = zswap_accept_thr_pages();
 
-	zswap_try_to_writeback(thr, zswap_total_pages());
+	zswap_try_to_writeback(NULL, thr, zswap_total_pages(), false);
 }
 
 /*********************************
@@ -1736,6 +1782,19 @@ int zswap_load(struct folio *folio)
 	return 0;
 }
 
+int zswap_proactive_writeback(struct mem_cgroup *memcg,

mm/zswap.c int zswap_load(struct folio *folio) · line 1795

Quoted 10 lines
mm/zswap.c +10 -0
+			      unsigned long nr_to_writeback)
+{
+	if (!memcg)
+		return -EINVAL;
+	if (!mem_cgroup_zswap_writeback_enabled(memcg))
+		return -EINVAL;
+	if (!nr_to_writeback)
+		return 0;
+
+	return zswap_try_to_writeback(memcg, 0, nr_to_writeback, true);

The memcg loop should be here, together with a check on the written bytes to check if the reclaim goal was achieved. I think nr_to_writeback is also very confusing, it's really the reclaim target in bytes divided by PAGE_SIZE. I think you need to pass in the number of bytes to reclaim/writeback directly.

Quoted 8 lines
mm/zswap.c +2 -0
+}
+
 void zswap_invalidate(swp_entry_t swp)
 {
 	pgoff_t offset = swp_offset(swp);
Signature · 1 line
2.34.1
author
#19 lore raw

PATCH v4 4/5 mm/zswap: Add per-memcg stat for proactive writeback

In reply to Hao Jia

From: Hao Jia <[email protected]>

Add a new stat zswpwb_proactive_b to memory.stat. This counter is incremented by entry->length during proactive writebacks triggered via the zswap_writeback_only key in memory.reclaim. It tracks the compressed size (in bytes) of pages proactively written back from zswap to swap, allowing users to better monitor and tune the proactive writeback mechanism.

---
 Documentation/admin-guide/cgroup-v2.rst |  4 ++++
 include/linux/memcontrol.h              |  1 +
 mm/memcontrol.c                         |  3 +++
 mm/zswap.c                              | 23 ++++++++++++++++++-----
 4 files changed, 26 insertions(+), 5 deletions(-)
Documentation/admin-guide/cgroup-v2.rst +4 -0
index e52d97e8e9c6..c164bb415002 100644
@@ -1748,6 +1748,10 @@ The following nested keys are defined.
 	  zswpwb
 		Number of pages written from zswap to swap.
 
+	  zswpwb_proactive_b
+		Bytes of compressed data proactively written back from
+		zswap to swap via memory.reclaim zswap_writeback_only key.
+
 	  zswap_incomp
 		Number of incompressible pages currently stored in zswap
 		without compression. These pages could not be compressed to
include/linux/memcontrol.h +1 -0
index e1f46a0016fc..56580b264dc4 100644
@@ -40,6 +40,7 @@ enum memcg_stat_item {
 	MEMCG_ZSWAP_B,
 	MEMCG_ZSWAPPED,
 	MEMCG_ZSWAP_INCOMP,
+	MEMCG_ZSWPWB_PROACTIVE_B,
 	MEMCG_NR_STAT,
 };
 
mm/memcontrol.c +3 -0
index 56cd4af08232..5ffb5095f0ee 100644
@@ -433,6 +433,7 @@ static const unsigned int memcg_stat_items[] = {
 	MEMCG_ZSWAP_B,
 	MEMCG_ZSWAPPED,
 	MEMCG_ZSWAP_INCOMP,
+	MEMCG_ZSWPWB_PROACTIVE_B,
 };
 
 #define NR_MEMCG_NODE_STAT_ITEMS ARRAY_SIZE(memcg_node_stat_items)
@@ -1558,6 +1559,7 @@ static const struct memory_stat memory_stats[] = {
 	{ "zswap",			MEMCG_ZSWAP_B			},
 	{ "zswapped",			MEMCG_ZSWAPPED			},
 	{ "zswap_incomp",		MEMCG_ZSWAP_INCOMP		},
+	{ "zswpwb_proactive_b",		MEMCG_ZSWPWB_PROACTIVE_B	},
 #endif
 	{ "file_mapped",		NR_FILE_MAPPED			},
 	{ "file_dirty",			NR_FILE_DIRTY			},
@@ -1614,6 +1616,7 @@ static int memcg_page_state_unit(int item)
 	switch (item) {
 	case MEMCG_PERCPU_B:
 	case MEMCG_ZSWAP_B:
+	case MEMCG_ZSWPWB_PROACTIVE_B:
 	case NR_SLAB_RECLAIMABLE_B:
 	case NR_SLAB_UNRECLAIMABLE_B:
 		return 1;
mm/zswap.c +18 -5
index 28200552dde3..d78bacf80209 100644
@@ -163,6 +163,7 @@ struct zswap_pool {
 struct zswap_shrink_walk_arg {
 	unsigned long bytes_written;
 	bool encountered_page_in_swapcache;
+	bool proactive;
 };
 
 /* Global LRU lists shared by all zswap pools. */
@@ -990,7 +991,8 @@ static bool zswap_decompress(struct zswap_entry *entry, struct folio *folio)
  * freed.
  */
 static int zswap_writeback_entry(struct zswap_entry *entry,
-				 swp_entry_t swpentry)
+				 swp_entry_t swpentry,
+				 bool proactive)
 {
 	struct xarray *tree;
 	pgoff_t offset = swp_offset(swpentry);
@@ -1045,6 +1047,15 @@ static int zswap_writeback_entry(struct zswap_entry *entry,
 	if (entry->objcg)
 		count_objcg_events(entry->objcg, ZSWPWB, 1);
 
+	if (proactive && entry->objcg) {
+		struct mem_cgroup *memcg;
+
+		rcu_read_lock();
+		memcg = obj_cgroup_memcg(entry->objcg);
+		mod_memcg_state(memcg, MEMCG_ZSWPWB_PROACTIVE_B, entry->length);
+		rcu_read_unlock();
+	}
+
 	zswap_entry_free(entry);
 
 	/* folio is up to date */
@@ -1155,7 +1166,7 @@ static enum lru_status shrink_memcg_cb(struct list_head *item, struct list_lru_o
 	 */
 	spin_unlock(&l->lock);
 
-	writeback_result = zswap_writeback_entry(entry, swpentry);
+	writeback_result = zswap_writeback_entry(entry, swpentry, walk_arg->proactive);
 
 	if (writeback_result) {
 		zswap_reject_reclaim_fail++;
@@ -1184,6 +1195,7 @@ static unsigned long zswap_shrinker_scan(struct shrinker *shrinker,
 	struct zswap_shrink_walk_arg walk_arg = {
 		.bytes_written = 0,
 		.encountered_page_in_swapcache = false,
+		.proactive = false,
 	};
 	unsigned long shrink_ret;
 
@@ -1305,11 +1317,12 @@ static struct shrinker *zswap_alloc_shrinker(void)
  * writeback disabled, is a zombie cgroup, or has empty zswap LRUs.
  */
 static long shrink_memcg(struct mem_cgroup *memcg,
-			 unsigned long nr_to_writeback)
+			 unsigned long nr_to_writeback, bool proactive)
 {
 	struct zswap_shrink_walk_arg walk_arg = {
 		.bytes_written = 0,
 		.encountered_page_in_swapcache = false,
+		.proactive = proactive,
 	};
 	u64 bytes_to_writeback = nr_to_writeback << PAGE_SHIFT;
 	bool memcg_list_is_empty = true;
@@ -1492,7 +1505,7 @@ static int zswap_try_to_writeback(struct mem_cgroup *memcg,
 		}
 
 		batch_size = min(upper_pages - lower_pages, NR_ZSWAP_WB_BATCH);
-		shrunk = shrink_memcg(iter_memcg, batch_size);
+		shrunk = shrink_memcg(iter_memcg, batch_size, proactive);
 		/* drop the extra reference */
 		mem_cgroup_put(iter_memcg);
 
@@ -1642,7 +1655,7 @@ bool zswap_store(struct folio *folio)
 	objcg = get_obj_cgroup_from_folio(folio);
 	if (objcg && !obj_cgroup_may_zswap(objcg)) {
 		memcg = get_mem_cgroup_from_objcg(objcg);
-		if (shrink_memcg(memcg, 1) <= 0) {
+		if (shrink_memcg(memcg, 1, false) <= 0) {
 			mem_cgroup_put(memcg);
 			goto put_objcg;
 		}
Signature · 1 line
2.34.1
maintainer
#20 lore raw

Re: [PATCH v4 4/5] mm/zswap: Add per-memcg stat for proactive writeback

[..]

mm/zswap.c static bool zswap_decompress(struct zswap_entry *entry, struct folio *folio) · line 995

Quoted 4 lines

static int zswap_writeback_entry(struct zswap_entry *entry,

mm/zswap.c +2 -1
-				 swp_entry_t swpentry)
+				 swp_entry_t swpentry,
+				 bool proactive)

IIUC, if we refactor the code as I suggested in previous changes, we don't really need to add an argument here..

Quoted 5 lines

{

mm/zswap.c +0 -0
 	struct xarray *tree;
 	pgoff_t offset = swp_offset(swpentry);
@@ -1045,6 +1047,15 @@ static int zswap_writeback_entry(struct zswap_entry *entry,
 	if (entry->objcg)

mm/zswap.c static int zswap_writeback_entry(struct zswap_entry *entry, · line 1057

Quoted 10 lines
mm/zswap.c +8 -0
 		count_objcg_events(entry->objcg, ZSWPWB, 1);
 
+	if (proactive && entry->objcg) {
+		struct mem_cgroup *memcg;
+
+		rcu_read_lock();
+		memcg = obj_cgroup_memcg(entry->objcg);
+		mod_memcg_state(memcg, MEMCG_ZSWPWB_PROACTIVE_B, entry->length);
+		rcu_read_unlock();
+	}

..and this chunk of code would end up in zswap_proactive_writeback().

author
#21 lore raw

PATCH v4 5/5 selftests/cgroup: Add tests for zswap proactive writeback

In reply to Hao Jia

From: Hao Jia <[email protected]>

Add test_zswap_proactive_writeback() to cover the new memory.reclaim "zswap_writeback_only" key. The test populates a memory cgroup zswap pool, triggers proactive writeback, and verifies the behavior by observing the change in zswpwb_proactive_b. Invalid input combinations are also covered.

Extend test_zswap_writeback_one() to assert that the existing non-proactive writeback path leaves zswpwb_proactive_b at zero.

---
 tools/testing/selftests/cgroup/test_zswap.c | 153 +++++++++++++++++++-
 1 file changed, 152 insertions(+), 1 deletion(-)
tools/testing/selftests/cgroup/test_zswap.c +152 -1
index 49b36ee79160..9c153fdd3a08 100644
@@ -60,7 +60,12 @@ static int get_zswap_stored_pages(size_t *value)
 
 static long get_cg_wb_count(const char *cg)
 {
-	return cg_read_key_long(cg, "memory.stat", "zswpwb");
+	return cg_read_key_long(cg, "memory.stat", "zswpwb ");
+}
+
+static long get_cg_pwb_bytes(const char *cg)
+{
+	return cg_read_key_long(cg, "memory.stat", "zswpwb_proactive_b ");
 }
 
 static long get_zswpout(const char *cgroup)
@@ -355,6 +360,7 @@ static int attempt_writeback(const char *cgroup, void *arg)
 static int test_zswap_writeback_one(const char *cgroup, bool wb)
 {
 	long zswpwb_before, zswpwb_after;
+	long pwb_bytes;
 
 	zswpwb_before = get_cg_wb_count(cgroup);
 	if (zswpwb_before != 0) {
@@ -362,6 +368,12 @@ static int test_zswap_writeback_one(const char *cgroup, bool wb)
 		return -1;
 	}
 
+	pwb_bytes = get_cg_pwb_bytes(cgroup);
+	if (pwb_bytes != 0) {
+		ksft_print_msg("zswpwb_proactive_b_before = %ld instead of 0\n", pwb_bytes);
+		return -1;
+	}
+
 	if (cg_run(cgroup, attempt_writeback, (void *) &wb))
 		return -1;
 
@@ -379,6 +391,17 @@ static int test_zswap_writeback_one(const char *cgroup, bool wb)
 		return -1;
 	}
 
+	/*
+	 * attempt_writeback() does not use the proactive writeback path, so
+	 * zswpwb_proactive_b must stay at zero regardless of whether
+	 * writeback was enabled.
+	 */
+	pwb_bytes = get_cg_pwb_bytes(cgroup);
+	if (pwb_bytes != 0) {
+		ksft_print_msg("zswpwb_proactive_b_after is %ld, expected 0\n", pwb_bytes);
+		return -1;
+	}
+
 	return 0;
 }
 
@@ -770,6 +793,133 @@ static int test_zswap_incompressible(const char *root)
 	return ret;
 }
 
+/*
+ * Trigger proactive zswap writeback with the following steps:
+ * 1. Allocate memory.
+ * 2. Push allocated memory into zswap.
+ * 3. Proactively write back zswap pages to swap
+ *    using "zswap_writeback_only".
+ */
+static int proactive_writeback_workload(const char *cgroup, void *arg)
+{
+	long pagesize = sysconf(_SC_PAGESIZE);
+	size_t memsize = pagesize * 1024;
+	char reclaim_cmd[64];
+	char buf[pagesize];
+	long zswap_usage;
+	int ret = -1;
+	int rc;
+	char *mem;
+
+	mem = (char *)malloc(memsize);
+	if (!mem)
+		return ret;
+
+	for (int i = 0; i < pagesize; i++)
+		buf[i] = i < pagesize / 2 ? (char)i : 0;
+	for (int i = 0; i < memsize; i += pagesize)
+		memcpy(&mem[i], buf, pagesize);
+
+	/* Evict allocated memory into zswap. */
+	if (cg_write_numeric(cgroup, "memory.reclaim", memsize)) {
+		ksft_print_msg("Failed to push pages into zswap\n");
+		goto out;
+	}
+
+	zswap_usage = cg_read_long(cgroup, "memory.zswap.current");
+	if (zswap_usage <= 0) {
+		ksft_print_msg("no zswap pool to write back\n");
+		goto out;
+	}
+
+	/* Trigger proactive zswap writeback. */
+	snprintf(reclaim_cmd, sizeof(reclaim_cmd), "%zu zswap_writeback_only", zswap_usage);
+	rc = cg_write(cgroup, "memory.reclaim", reclaim_cmd);
+	if (rc && rc != -EAGAIN) {
+		ksft_print_msg("proactive zswap writeback failed: %d\n", rc);
+		goto out;
+	}
+
+	ret = 0;
+out:
+	free(mem);
+	return ret;
+}
+
+static int check_writeback_invalid_inputs(const char *cgroup)
+{
+	static char * const bad_inputs[] = {
+		"zswap_writeback_only",
+		"1M zswap_writeback_only swappiness=60",
+		"1M swappiness=60 zswap_writeback_only",
+		"1M zswap_writeback_only swappiness=max",
+		"1M swappiness=max zswap_writeback_only",
+	};
+	int i, rc;
+
+	for (i = 0; i < ARRAY_SIZE(bad_inputs); i++) {
+		rc = cg_write(cgroup, "memory.reclaim", bad_inputs[i]);
+		if (rc != -EINVAL) {
+			ksft_print_msg("memory.reclaim '%s': returned %d, expected %d\n",
+				       bad_inputs[i], rc, -EINVAL);
+			return -1;
+		}
+	}
+	return 0;
+}
+
+static int test_zswap_proactive_writeback(const char *root)
+{
+	long wb_before, wb_after;
+	long pwb_b_before, pwb_b_after;
+	long wb_delta, pwb_b_delta;
+	int ret = KSFT_FAIL;
+	char *test_group;
+
+	if (cg_read_strcmp(root, "memory.zswap.writeback", "1"))
+		return KSFT_SKIP;
+
+	test_group = cg_name(root, "zswap_proactive_test");
+	if (!test_group)
+		return KSFT_FAIL;
+	if (cg_create(test_group))
+		goto out;
+	if (check_writeback_invalid_inputs(test_group))
+		goto out;
+
+	pwb_b_before = get_cg_pwb_bytes(test_group);
+	wb_before = get_cg_wb_count(test_group);
+	if (pwb_b_before < 0 || wb_before < 0)
+		goto out;
+
+	if (cg_run(test_group, proactive_writeback_workload, NULL))
+		goto out;
+
+	pwb_b_after = get_cg_pwb_bytes(test_group);
+	wb_after = get_cg_wb_count(test_group);
+	if (pwb_b_after < 0 || wb_after < 0)
+		goto out;
+
+	pwb_b_delta = pwb_b_after - pwb_b_before;
+	wb_delta = wb_after - wb_before;
+
+	if (pwb_b_delta <= 0) {
+		ksft_print_msg("zswpwb_proactive_b did not increase: delta=%ld\n",
+			       pwb_b_delta);
+		goto out;
+	}
+	if (wb_delta <= 0) {
+		ksft_print_msg("zswpwb did not increase: delta=%ld\n", wb_delta);
+		goto out;
+	}
+
+	ret = KSFT_PASS;
+out:
+	cg_destroy(test_group);
+	free(test_group);
+	return ret;
+}
+
 #define T(x) { x, #x }
 struct zswap_test {
 	int (*fn)(const char *root);
@@ -783,6 +933,7 @@ struct zswap_test {
 	T(test_no_kmem_bypass),
 	T(test_no_invasive_cgroup_shrink),
 	T(test_zswap_incompressible),
+	T(test_zswap_proactive_writeback),
 };
 #undef T
 
Signature · 1 line
2.34.1
reviewer
#22 lore raw
In reply to Hao Jia
Quoted 25 lines · Hao Jia

From: Hao Jia <[email protected]>

Zswap currently writes back pages to backing swap reactively, triggered either by the shrinker or by the pool reaching its size limit. Although proactive memory reclaim can automatically write back a portion of zswap pages via the shrinker, it cannot explicitly control the amount of writeback for a specific memory cgroup. Moreover, proactive memory reclaim may not always be triggered during a steady state.

In certain scenarios, it is desirable to trigger writeback in advance to free up memory. For example, users may want to prepare for an upcoming memory-intensive workload by flushing cold memory to the backing storage when the system is relatively idle.

This patch series introduces a "zswap_writeback_only" key to memory.reclaim cgroup interface, allowing users to proactively write back cold compressed data from zswap to the backing swap device. When specified, this key bypasses standard memory reclaim and exclusively performs proactive zswap writeback up to the requested budget. If omitted, the default reclaim behavior remains unchanged.

Quoted 3 lines

Example usage:
# Write back 10MB of compressed data from zswap to the backing swap echo "10M zswap_writeback_only" > memory.reclaim

I’m not entirely sure if other candidate names were already brought up in previous discussions, so my apologies if I'm repeating something here! I do think expanding memory.reclaim is a great approach. That said, I was wondering if we could make the interface a bit more concise while keeping it flexible for future extensions.

Essentially, what we want is to control the specific targets of the reclaim process—such as file, anon, or zswap. What do you think about using something like "source=zswap"? For instance, if we want to reclaim 10M from zswap, the command would look like this:

echo "10M source=zswap" > memory.reclaim

If we only want to reclaim 10M from file pages, we could easily extend the syntax:

echo "10M source=file" > memory.reclaim

And of course, we could even combine them down the road:

echo "10M source=anon,file" > memory.reclaim

to only reclaim anon and file but bypass zswap.

Just some thoughts of mine.

Muchun,
Thanks

Quoted 60 lines

Patch 1: Extend shrink_memcg() to support batch writeback based on a
compressed-size budget and update its return value semantics, thereby improving the writeback efficiency in the shrink_worker() path.
Patch 2: Extract the memcg iteration and writeback loop into helper
functions to prepare for proactive writeback.
Patch 3: Extend the memory.reclaim cgroup v2 interface with a new
"zswap_writeback_only" key, allowing users to trigger proactive zswap writeback up to a requested budget.
Patch 4: Add the zswpwb_proactive_b stat to track the compressed bytes
of proactive writeback for better monitoring and tuning.
Patch 5:
Add tests for zswap proactive writeback.

v3->v4:
- Drop the per-memcg cursor and keep the root cgroup cursor (zswap_next_shrink) logic intact.
- Stick to using the zswap_writeback_only key, and change the proactive writeback size to use the compressed size.
- Consolidate and reuse the logic between shrink_worker() and shrink_memcg(). Enable batch writeback in the shrink_worker() path, while maintaining a low writeback budget in the zswap_store() path.

v2->v3:
- Align the return value of zswap_proactive_writeback() with memory.reclaim and update the corresponding documentation accordingly.
- Resolve conflicts in test_zswap.c on the mm-unstable branch.
- Enhance the zswap proactive writeback selftests to guard against potential future regressions.

v1->v2:
- As suggested by Yosry and Nhat, extend the memory.reclaim cgroup v2 interface with a "zswap_writeback_only" key instead of adding a new dedicated cgroup interface.
- Update the zswap documentation and add selftests for proactive writeback.

[v3] https://lore.kernel.org/all/[email protected]
[v2] https://lore.kernel.org/all/[email protected]
[v1] https://lore.kernel.org/all/[email protected]

Hao Jia (5):
mm/zswap: Extend shrink_memcg() writeback capability mm/zswap: Factor writeback loop out of shrink_worker() mm/zswap: Implement proactive writeback mm/zswap: Add per-memcg stat for proactive writeback selftests/cgroup: Add tests for zswap proactive writeback

Documentation/admin-guide/cgroup-v2.rst     |  22 +-
Documentation/admin-guide/mm/zswap.rst      |  11 +-
include/linux/memcontrol.h                  |   1 +
include/linux/zswap.h                       |   7 +
mm/memcontrol.c                             |   3 +
mm/vmscan.c                                 |  14 +
mm/zswap.c                                  | 322 +++++++++++++++-----
tools/testing/selftests/cgroup/test_zswap.c | 153 +++++++++-
8 files changed, 456 insertions(+), 77 deletions(-)
Signature · 1 line
2.34.1
author
#23 lore raw
Quoted 35 lines · Muchun Song
Quoted 25 lines · Hao Jia

From: Hao Jia <[email protected]>

Zswap currently writes back pages to backing swap reactively, triggered either by the shrinker or by the pool reaching its size limit. Although proactive memory reclaim can automatically write back a portion of zswap pages via the shrinker, it cannot explicitly control the amount of writeback for a specific memory cgroup. Moreover, proactive memory reclaim may not always be triggered during a steady state.

In certain scenarios, it is desirable to trigger writeback in advance to free up memory. For example, users may want to prepare for an upcoming memory-intensive workload by flushing cold memory to the backing storage when the system is relatively idle.

This patch series introduces a "zswap_writeback_only" key to memory.reclaim cgroup interface, allowing users to proactively write back cold compressed data from zswap to the backing swap device. When specified, this key bypasses standard memory reclaim and exclusively performs proactive zswap writeback up to the requested budget. If omitted, the default reclaim behavior remains unchanged.

Quoted 3 lines

Example usage:
# Write back 10MB of compressed data from zswap to the backing swap echo "10M zswap_writeback_only" > memory.reclaim

I’m not entirely sure if other candidate names were already brought up in previous discussions, so my apologies if I'm repeating something here! I do think expanding memory.reclaim is a great approach. That said, I was wondering if we could make the interface a bit more concise while keeping it flexible for future extensions.

Quoted 7 lines

Essentially, what we want is to control the specific targets of the reclaim process—such as file, anon, or zswap. What do you think about using something like "source=zswap"? For instance, if we want to reclaim 10M from zswap, the command would look like this:

echo "10M source=zswap" > memory.reclaim

Thanks for the suggestion. TBH, I personally think your approach makes more sense than "zswap_writeback_only".

Hi YoungJun and Yosry,

I am not sure if this suggestion from Muchun could decouple zswap proactive writeback from the swap tiers, or make it easier to migrate to swap tiers in the future:

echo "10M source=zswap" > memory.reclaim

For now, we only specify the source. Later on, the swap tiers feature could extend this to control whether to demote to SSD swap, HDD swap, or other tiers.

Thanks,
Hao

Quoted 15 lines

If we only want to reclaim 10M from file pages, we could easily extend the syntax:

echo "10M source=file" > memory.reclaim

And of course, we could even combine them down the road:

echo "10M source=anon,file" > memory.reclaim

to only reclaim anon and file but bypass zswap.

Just some thoughts of mine.

Muchun,
Thanks

#24 lore raw
Quoted 55 lines · Hao Jia
Quoted 35 lines · Muchun Song
Quoted 25 lines · Hao Jia

From: Hao Jia <[email protected]>

Zswap currently writes back pages to backing swap reactively, triggered either by the shrinker or by the pool reaching its size limit. Although proactive memory reclaim can automatically write back a portion of zswap pages via the shrinker, it cannot explicitly control the amount of writeback for a specific memory cgroup. Moreover, proactive memory reclaim may not always be triggered during a steady state.

In certain scenarios, it is desirable to trigger writeback in advance to free up memory. For example, users may want to prepare for an upcoming memory-intensive workload by flushing cold memory to the backing storage when the system is relatively idle.

This patch series introduces a "zswap_writeback_only" key to memory.reclaim cgroup interface, allowing users to proactively write back cold compressed data from zswap to the backing swap device. When specified, this key bypasses standard memory reclaim and exclusively performs proactive zswap writeback up to the requested budget. If omitted, the default reclaim behavior remains unchanged.

Quoted 3 lines

Example usage:
# Write back 10MB of compressed data from zswap to the backing swap echo "10M zswap_writeback_only" > memory.reclaim

I’m not entirely sure if other candidate names were already brought up in previous discussions, so my apologies if I'm repeating something here! I do think expanding memory.reclaim is a great approach. That said, I was wondering if we could make the interface a bit more concise while keeping it flexible for future extensions.

Quoted 7 lines

Essentially, what we want is to control the specific targets of the reclaim process—such as file, anon, or zswap. What do you think about using something like "source=zswap"? For instance, if we want to reclaim 10M from zswap, the command would look like this:

echo "10M source=zswap" > memory.reclaim

Thanks for the suggestion. TBH, I personally think your approach makes more sense than "zswap_writeback_only".

Hi YoungJun and Yosry,

I am not sure if this suggestion from Muchun could decouple zswap proactive writeback from the swap tiers, or make it easier to migrate to swap tiers in the future:

Quoted 7 lines

echo "10M source=zswap" > memory.reclaim
For now, we only specify the source. Later on, the swap tiers feature could extend this to control whether to demote to SSD swap, HDD swap, or other tiers.

Thanks,
Hao

Hi Hao!

I also preferred sharing the memory.reclaim interface in the future swap demotion, since it already takes zswap_writeback_only.
https://lore.kernel.org/all/aieUQUBHI+E3uNPW@yjaykim-PowerEdge-T330/

Alternatively, we could use a separate interface as Yosry suggested (e.g. 'swap.tiers.demote'?).

But as Nhat pointed out, allowing user-triggered demotion from the swap tier perspective could lead to issues like LRU inversion. We probably need to discuss whether this kind of user-triggered tier demotion will actually be supported at all.
https://lore.kernel.org/linux-mm/CAKEwX=NfSy0XiD_UMsDOHGCwpE7sYmBmhV4Y9vk_cbnnr6J6PQ@mail.gmail.com/

So, IMHO..

1. If swap tier demotion is NOT exposed.

We can simply choose between "source=" and zswap_writeback_only based on preference. (since there is no need to consider "swap_tier" demotion.)

However, "source=" seems to offer better extensibility if it is expanded to file and anon use cases in the future.

2. If swap tier demotion IS exposed.
We need to consider integration vs decoupling.

(In my view, This is a design consideration. avoiding potentially redundant interfaces vs adding a new one if it is architecturally correct.)

2.1 Integration
- Integrating into 'memory.reclaim':
- "source=": Seems easier to integrate by explicitly specifying the target. (Your suggestion)
- 'zswap_writeback_only': Harder to integrate than "source=".

- Integrating into 'memory.swap.tiers.demote'
- 'memory.swap.tiers.demote' could absorb the memory.reclaim functionality. (But since we only want to allow tiering for vswap+zswap cases like the zswap writeback feature as we discussed, the reclaim interface behavior might still need to stay for zswap only.)

2.2 Decoupling
- 'memory.swap.tiers.demote' handles other swap devices (excluding zswap),
while "source=" or 'zswap_writeback_only' handles only zswap.

I think future discussions might lean toward "integrating into 'memory.swap.tiers.demote'". Therefore, from this perspective, either direction seems fine. However, I slightly prefer "source=" due to its potential for other extensions.

I don't have a strong preference, though!

Thanks
Youngjun

Quoted 12 lines

If we only want to reclaim 10M from file pages, we could easily extend the syntax:

echo "10M source=file" > memory.reclaim

And of course, we could even combine them down the road:

echo "10M source=anon,file" > memory.reclaim

to only reclaim anon and file but bypass zswap.

Just some thoughts of mine.

maintainer
#25 lore raw
Quoted 40 lines · Youngjun Park
Quoted 38 lines · Hao Jia
Quoted 35 lines · Muchun Song
Quoted 25 lines · Hao Jia

From: Hao Jia <[email protected]>

Zswap currently writes back pages to backing swap reactively, triggered either by the shrinker or by the pool reaching its size limit. Although proactive memory reclaim can automatically write back a portion of zswap pages via the shrinker, it cannot explicitly control the amount of writeback for a specific memory cgroup. Moreover, proactive memory reclaim may not always be triggered during a steady state.

In certain scenarios, it is desirable to trigger writeback in advance to free up memory. For example, users may want to prepare for an upcoming memory-intensive workload by flushing cold memory to the backing storage when the system is relatively idle.

This patch series introduces a "zswap_writeback_only" key to memory.reclaim cgroup interface, allowing users to proactively write back cold compressed data from zswap to the backing swap device. When specified, this key bypasses standard memory reclaim and exclusively performs proactive zswap writeback up to the requested budget. If omitted, the default reclaim behavior remains unchanged.

Quoted 3 lines

Example usage:
# Write back 10MB of compressed data from zswap to the backing swap echo "10M zswap_writeback_only" > memory.reclaim

I’m not entirely sure if other candidate names were already brought up in previous discussions, so my apologies if I'm repeating something here! I do think expanding memory.reclaim is a great approach. That said, I was wondering if we could make the interface a bit more concise while keeping it flexible for future extensions.

Quoted 6 lines
Quoted 6 lines
Quoted 6 lines

Essentially, what we want is to control the specific targets of the reclaim process—such as file, anon, or zswap. What do you think about using something like "source=zswap"? For instance, if we want to reclaim 10M from zswap, the command would look like this:

echo "10M source=zswap" > memory.reclaim

I like this suggestion, but I think ultimately we want proactive zswap writeback to be part of a more general proactive swap demotion, and zswap is just a swap tier.

Quoted 1 line
Quoted 1 line
Quoted 1 line

[..]

Quoted 5 lines

I also preferred sharing the memory.reclaim interface in the future swap demotion, since it already takes zswap_writeback_only.
https://lore.kernel.org/all/aieUQUBHI+E3uNPW@yjaykim-PowerEdge-T330/

Quoted 8 lines

Alternatively, we could use a separate interface as Yosry suggested (e.g. 'swap.tiers.demote'?).

But as Nhat pointed out, allowing user-triggered demotion from the swap tier perspective could lead to issues like LRU inversion. We probably need to discuss whether this kind of user-triggered tier demotion will actually be supported at all.
https://lore.kernel.org/linux-mm/CAKEwX=NfSy0XiD_UMsDOHGCwpE7sYmBmhV4Y9vk_cbnnr6J6PQ@mail.gmail.com/

I believe what Nhat said is that swap demotion may be used to prevent/alleviate LRU inversion, not cause it. I don't see how demotion can cause LRU inversion.

Quoted 28 lines

So, IMHO..

1. If swap tier demotion is NOT exposed.

We can simply choose between "source=" and zswap_writeback_only based on preference. (since there is no need to consider "swap_tier" demotion.)

However, "source=" seems to offer better extensibility if it is expanded to file and anon use cases in the future.

2. If swap tier demotion IS exposed.
We need to consider integration vs decoupling.

(In my view, This is a design consideration. avoiding potentially redundant interfaces vs adding a new one if it is architecturally correct.)

2.1 Integration
- Integrating into 'memory.reclaim':
- "source=": Seems easier to integrate by explicitly specifying the target. (Your suggestion)
- 'zswap_writeback_only': Harder to integrate than "source=".

- Integrating into 'memory.swap.tiers.demote'
- 'memory.swap.tiers.demote' could absorb the memory.reclaim functionality. (But since we only want to allow tiering for vswap+zswap cases like the zswap writeback feature as we discussed, the reclaim interface behavior might still need to stay for zswap only.)

Quoted 3 lines

2.2 Decoupling
- 'memory.swap.tiers.demote' handles other swap devices (excluding zswap),
while "source=" or 'zswap_writeback_only' handles only zswap.

I personally think making proactive zswap writeback one use case of proactive swap demotion makes sense. I think swap demotion in general makes sense.