Recent changes (master)

Jens Axboe <[email protected]> Tue, 10 Feb 2026 06:00:01 -0700
Newsgroups org.kernel.vger.fio
Message-ID <[email protected]>
The following changes since commit 6783ccc569c8e427cb3cfe4e8b37e18fa0601d9f:

  options: ensure callback handlers handle NULL input (2026-02-08 08:39:27 -0700)

are available in the Git repository at:

  git://git.kernel.dk/fio.git master

for you to fetch changes up to 05b2e9fe7722af4470139465a53c629c40936cb4:

  fio: bump server version for new option (2026-02-10 07:36:17 -0500)

----------------------------------------------------------------
Charles Henry (1):
      SPRandom Cache Size Behavior Implementation

Vincent Fu (2):
      Merge branch 'sprandom-cache-implementation' of https://github.com/cachyyyk/fio-spr-cache
      fio: bump server version for new option

 HOWTO.rst        | 18 +++++++++++++
 cconv.c          |  2 ++
 fio.1            | 18 +++++++++++++
 options.c        | 13 ++++++++++
 server.h         |  2 +-
 sprandom.c       | 77 +++++++++++++++++++++++++++++++++++++++++++++++---------
 sprandom.h       |  1 +
 t/sprandom.py    | 41 +++++++++++++++++++++++++++++-
 thread_options.h |  2 ++
 9 files changed, 160 insertions(+), 14 deletions(-)

---

Diff of recent changes:

diff --git a/HOWTO.rst b/HOWTO.rst
index fcb8a914..d31851e9 100644
--- a/HOWTO.rst
+++ b/HOWTO.rst
@@ -1674,6 +1674,24 @@ I/O type
         Default=0.15
 
 
+.. option:: spr_cs=int
+
+	See :option:`sprandom`. Define a cache size in bytes, as specified
+	    by the SSD manufacturer. When this is non-zero, delay invalidating
+		writes by one region in order to make sure that all original
+		writes from a region are flushed from cache before the later
+		invalidating writes are sent to the device. This deferral
+		prevents the original write and the later invalidating write
+		from being present in the device's cache at the same time which
+		would allow the device to ignore the original write and prevent
+		sprandom from achieving its target validity fractions. The
+		actual cache size is used to ensure that the number of regions
+		is not set so large that the size of a region is smaller than
+		the device cache.
+
+        Default=0
+
+
 Block size
 ~~~~~~~~~~
 
diff --git a/cconv.c b/cconv.c
index 3d7b3d14..9f82c724 100644
--- a/cconv.c
+++ b/cconv.c
@@ -233,6 +233,7 @@ int convert_thread_options_to_cpu(struct thread_options *o,
 	o->sprandom = le32_to_cpu(top->sprandom);
 	o->spr_num_regions = le32_to_cpu(top->spr_num_regions);
 	o->spr_over_provisioning.u.f = fio_uint64_to_double(le64_to_cpu(top->spr_over_provisioning.u.i));
+	o->spr_cache_size = le64_to_cpu(top->spr_cache_size);
 	o->bs_unaligned = le32_to_cpu(top->bs_unaligned);
 	o->fsync_on_close = le32_to_cpu(top->fsync_on_close);
 	o->bs_is_seq_rand = le32_to_cpu(top->bs_is_seq_rand);
@@ -486,6 +487,7 @@ void convert_thread_options_to_net(struct thread_options_pack *top,
 	top->sprandom = cpu_to_le32(o->sprandom);
 	top->spr_num_regions = cpu_to_le32(o->spr_num_regions);
 	top->spr_over_provisioning.u.i = __cpu_to_le64(fio_double_to_uint64(o->spr_over_provisioning.u.f));
+	top->spr_cache_size = __cpu_to_le64(o->spr_cache_size);
 	top->bs_unaligned = cpu_to_le32(o->bs_unaligned);
 	top->fsync_on_close = cpu_to_le32(o->fsync_on_close);
 	top->bs_is_seq_rand = cpu_to_le32(o->bs_is_seq_rand);
diff --git a/fio.1 b/fio.1
index 014916ea..bc3efa5f 100644
--- a/fio.1
+++ b/fio.1
@@ -1469,6 +1469,24 @@ For large devices it is better to use more regions, to increase precision
 and reduce memory allocation. The allocation is proportional to the region size.
 .RE
 .TP
+.B spr_cs=int
+See
+.BR sprandom .
+Define a cache size in bytes, as specified by the SSD manufacturer.
+.P
+.RS
+When this is non-zero, delay invalidating writes by one region in order
+to make sure that all original writes from a region are flushed from
+cache before the later invalidating writes are sent to the device.
+This deferral prevents the original write and the later invalidating
+write from being present in the device's cache at the same time which
+would allow the device to ignore the original write and prevent
+sprandom from achieving its target validity fractions. The actual
+cache size is used to ensure that the number of regions is not set
+so large that the size of a region is smaller than the device cache.
+The default is 0.
+.RE
+.TP
 .B spr_op=float
 See
 .BR sprandom .
diff --git a/options.c b/options.c
index 68189be7..f592bc24 100644
--- a/options.c
+++ b/options.c
@@ -2710,6 +2710,19 @@ struct fio_option fio_options[FIO_MAX_OPTS] = {
 		.category = FIO_OPT_C_IO,
 		.group	= FIO_OPT_G_RANDOM,
 	},
+	{
+		.name	= "spr_cs",
+		.lname	= "SPRandom Device cache size",
+		.type	= FIO_OPT_ULL,
+		.off1	= offsetof(struct thread_options, spr_cache_size),
+		.help	= "Cache Size in bytes for SPRandom",
+		.parent	= "sprandom",
+		.maxlen = 1,
+		.hide	= 1,
+		.def    = "0",
+		.category = FIO_OPT_C_IO,
+		.group	= FIO_OPT_G_RANDOM,
+	},
 	{
 		.name	= "random_generator",
 		.lname	= "Random Generator",
diff --git a/server.h b/server.h
index a1e71a44..e0a921b8 100644
--- a/server.h
+++ b/server.h
@@ -51,7 +51,7 @@ struct fio_net_cmd_reply {
 };
 
 enum {
-	FIO_SERVER_VER			= 117,
+	FIO_SERVER_VER			= 118,
 
 	FIO_SERVER_MAX_FRAGMENT_PDU	= 1024,
 	FIO_SERVER_MAX_CMD_MB		= 2048,
diff --git a/sprandom.c b/sprandom.c
index 5565b1a1..429a7754 100644
--- a/sprandom.c
+++ b/sprandom.c
@@ -570,6 +570,7 @@ static int sprandom_setup(struct sprandom_info *spr_info, uint64_t logical_size,
 			  uint64_t align_bs)
 {
 	double over_provisioning = spr_info->over_provisioning;
+	int ret = 0;
 	uint64_t physical_size;
 	uint64_t region_sz;
 	uint64_t region_write_count;
@@ -584,8 +585,10 @@ static int sprandom_setup(struct sprandom_info *spr_info, uint64_t logical_size,
 
 	validity_dist = compute_validity_dist(spr_info->num_regions,
 					      spr_info->over_provisioning);
-	if (!validity_dist)
-		return -ENOMEM;
+	if (!validity_dist) {
+		ret = -ENOMEM;
+		goto err;
+	}
 
 	/* Initialize validity_distribution */
 	print_d_array("validity resampled:", validity_dist, spr_info->num_regions);
@@ -593,8 +596,10 @@ static int sprandom_setup(struct sprandom_info *spr_info, uint64_t logical_size,
 	/* Precompute invalidity percentage array */
 	spr_info->invalid_pct = calloc(spr_info->num_regions,
 				       sizeof(spr_info->invalid_pct[0]));
-	if (!spr_info->invalid_pct)
+	if (!spr_info->invalid_pct) {
+		ret = -ENOMEM;
 		goto err;
+	}
 
 	total_alloc += spr_info->num_regions * sizeof(spr_info->invalid_pct[0]);
 
@@ -606,8 +611,24 @@ static int sprandom_setup(struct sprandom_info *spr_info, uint64_t logical_size,
 	region_sz = physical_size / spr_info->num_regions;
 	region_write_count = region_sz / align_bs;
 
-	invalid_capacity = estimate_inv_capacity(region_write_count,
-						 validity_dist[0]);
+	if ((spr_info->cache_sz) && (spr_info->cache_sz > region_sz)) {
+		log_err("fio: sprandom: spr_cs [%"PRIu64"] must be smaller than"
+			" region_sz [%"PRIu64"] which means [%"PRIu64"] regions"
+			" allowed", spr_info->cache_sz, region_sz,
+		    (physical_size / spr_info->cache_sz));
+		ret = -EINVAL;
+		goto err;
+	}
+
+	if (spr_info->cache_sz) {
+		/* Need 2x size to be safe since we wait to invalidate until after next region */
+		invalid_capacity = estimate_inv_capacity(region_write_count,
+							validity_dist[0]) * 2;
+	} else {
+		invalid_capacity = estimate_inv_capacity(region_write_count,
+							validity_dist[0]);
+	}
+
 	spr_info->invalid_capacity = invalid_capacity;
 
 	spr_info->invalid_buf = pcb_alloc(invalid_capacity);
@@ -633,6 +654,10 @@ static int sprandom_setup(struct sprandom_info *spr_info, uint64_t logical_size,
 	dprint(FD_SPRANDOM, "  op: %02f\n", spr_info->over_provisioning);
 	dprint(FD_SPRANDOM, "  region_size:        %"PRIu64"\n", region_sz);
 	dprint(FD_SPRANDOM, "  num_regions:        %u\n", spr_info->num_regions);
+	dprint(FD_SPRANDOM, "  cache_size:         %"PRIu64": %s\n",
+		spr_info->cache_sz,
+		bytes2str_simple(bytes2str_buf, sizeof(bytes2str_buf),
+						 spr_info->cache_sz));
 	dprint(FD_SPRANDOM, "  region_write_count: %"PRIu64"\n", region_write_count);
 	dprint(FD_SPRANDOM, "  invalid_capacity:   %zu\n", invalid_capacity);
 	dprint(FD_SPRANDOM, "  dynamic memory:     %zu: %s\n",
@@ -644,7 +669,7 @@ static int sprandom_setup(struct sprandom_info *spr_info, uint64_t logical_size,
 err:
 	free(validity_dist);
 	free(spr_info->invalid_pct);
-	return -ENOMEM;
+	return ret;
 }
 
 /**
@@ -717,16 +742,32 @@ int sprandom_get_next_offset(struct sprandom_info *info, struct fio_file *f, uin
 	uint64_t offset = 0;
 	uint32_t phase = info->curr_phase;
 
-	/* replay invalidation */
-	if (pcb_pop(info->invalid_buf, &offset)) {
-		sprandom_add_with_probability(info, offset,  phase ^ 1);
-		dprint(FD_SPRANDOM, "Write %"PRIu64" over %d\n",
-		       offset, info->current_region);
-		goto out;
+	if (!info->cache_sz) {
+		/* replay invalidation at start of next region prior to moving
+		 * to new region.
+		 */
+		if (pcb_pop(info->invalid_buf, &offset)) {
+			sprandom_add_with_probability(info, offset,  phase ^ 1);
+			dprint(FD_SPRANDOM, "Write %"PRIu64" over %d\n",
+				offset, info->current_region);
+			goto out;
+		}
 	}
 
 	/* Move to next region */
 	if (info->writes_remaining == 0) {
+		if (info->cache_sz) {
+			/* replay invalidation for previous region at end of this
+			 * region to avoid invalidations hitting the defined cache.
+			 */
+			if (pcb_pop(info->invalid_buf, &offset)) {
+				sprandom_add_with_probability(info, offset,  phase ^ 1);
+				dprint(FD_SPRANDOM, "Cache Defer Write %"PRIu64" "
+					" over %d\n", offset, info->current_region);
+				goto out;
+			}
+		}
+
 		if (info->current_region >= info->num_regions) {
 			dprint(FD_SPRANDOM, "End: Last Region %d cur%d\n",
 			       info->current_region, info->num_regions);
@@ -747,6 +788,17 @@ int sprandom_get_next_offset(struct sprandom_info *info, struct fio_file *f, uin
 
 	/* Fetch new offset */
 	if (lfsr_next(&f->lfsr, &offset)) {
+		if (info->cache_sz) {
+			/* Since we defer invalidation to the end of next region we
+			 * need to take into account end of lfsr case
+			 */
+			if (pcb_pop(info->invalid_buf, &offset)) {
+				dprint(FD_SPRANDOM, "lfsr cache exit Write %"PRIu64" "
+					" over %d\n", offset, info->current_region);
+				goto out;
+			}
+		}
+
 		dprint(FD_SPRANDOM, "End: LFSR exhausted %d [%zu] [%zu]\n",
 			info->current_region,
 			info->invalid_count[phase],
@@ -802,6 +854,7 @@ int sprandom_init(struct thread_data *td, struct fio_file *f)
 	over_provisioning = td->o.spr_over_provisioning.u.f;
 	info->num_regions = td->o.spr_num_regions;
 	info->over_provisioning = over_provisioning;
+	info->cache_sz = td->o.spr_cache_size;
 	td->o.io_size = sprandom_physical_size(over_provisioning,
 					       logical_size, align_bs);
 	info->rand_state = &td->sprandom_state;
diff --git a/sprandom.h b/sprandom.h
index 175df8f5..d50c5afb 100644
--- a/sprandom.h
+++ b/sprandom.h
@@ -31,6 +31,7 @@
 struct sprandom_info {
 	double    over_provisioning;
 	uint64_t  region_sz;
+	uint64_t  cache_sz;
 	uint32_t  num_regions;
 
 	uint32_t  *invalid_pct;
diff --git a/t/sprandom.py b/t/sprandom.py
index e1b3a5e0..a4b2fcbc 100755
--- a/t/sprandom.py
+++ b/t/sprandom.py
@@ -25,6 +25,7 @@ from fiotestcommon import SUCCESS_DEFAULT, SUCCESS_NONZERO
 SPRANDOM_OPT_LIST = [
     'spr_op',
     'spr_num_regions',
+    'spr_cs',
     'size',
     'norandommap',
     'random_generator',
@@ -66,6 +67,7 @@ TEST_LIST = [
         "fio_opts": {
             "spr_op": "0.10",
             "spr_num_regions": "50",
+            "spr_cs": "0",
             "size": "32M",
         },
         "success": SUCCESS_DEFAULT,
@@ -76,6 +78,7 @@ TEST_LIST = [
         "fio_opts": {
             "spr_op": "0.25",
             "spr_num_regions": "100",
+            "spr_cs": "0",
             "size": "64M",
         },
         "success": SUCCESS_DEFAULT,
@@ -86,6 +89,7 @@ TEST_LIST = [
         "fio_opts": {
             "spr_op": "0.50",
             "spr_num_regions": "200",
+            "spr_cs": "0",
             "size": "128M",
             "random_generator": "tausworthe",
         },
@@ -97,6 +101,7 @@ TEST_LIST = [
         "fio_opts": {
             "spr_op": "0.75",
             "spr_num_regions": "400",
+            "spr_cs": "0",
             "size": "256M",
             "norandommap": "0"
         },
@@ -105,10 +110,11 @@ TEST_LIST = [
         "test_class": FioSPrandomTest,
     },
     {
-        "test_id": 4,
+        "test_id": 5,
         "fio_opts": {
             "spr_op": "0.75",
             "spr_num_regions": "400",
+            "spr_cs": "0",
             "size": "256M",
             "rw": "randread",
         },
@@ -116,6 +122,39 @@ TEST_LIST = [
         "success": SUCCESS_NONZERO,
         "test_class": FioSPrandomTest,
     },
+    {
+        "test_id": 6,
+        "fio_opts": {
+            "spr_op": "0.10",
+            "spr_num_regions": "100",
+            "spr_cs": "32K",
+            "size": "32M",
+        },
+        "success": SUCCESS_DEFAULT,
+        "test_class": FioSPrandomTest,
+    },
+    {
+        "test_id": 7,
+        "fio_opts": {
+            "spr_op": "0.10",
+            "spr_num_regions": "2000",
+            "spr_cs": "32K",
+            "size": "32M",
+        },
+        "success": SUCCESS_NONZERO,
+        "test_class": FioSPrandomTest,
+    },
+    {
+        "test_id": 8,
+        "fio_opts": {
+            "spr_op": "0.10",
+            "spr_num_regions": "50",
+            "spr_cs": "32M",
+            "size": "32M",
+        },
+        "success": SUCCESS_NONZERO,
+        "test_class": FioSPrandomTest,
+    },
 ]
 
 
diff --git a/thread_options.h b/thread_options.h
index 4a52f981..3e66d477 100644
--- a/thread_options.h
+++ b/thread_options.h
@@ -180,6 +180,7 @@ struct thread_options {
 	unsigned int softrandommap;
 	unsigned int sprandom;
 	unsigned int spr_num_regions;
+	unsigned long long spr_cache_size;
 	fio_fp64_t spr_over_provisioning;
 	unsigned int bs_unaligned;
 	unsigned int fsync_on_close;
@@ -517,6 +518,7 @@ struct thread_options_pack {
 	uint32_t softrandommap;
 	uint32_t sprandom;
 	uint32_t spr_num_regions;
+	uint64_t spr_cache_size;
 	fio_fp64_t spr_over_provisioning;
 	uint32_t bs_unaligned;
 	uint32_t fsync_on_close;