All of lore.kernel.org
 help / color / mirror / Atom feed
From: Luis Chamberlain <mcgrof@kernel.org>
To: vincent.fu@samsung.com, fio@vger.kernel.org
Cc: mcgrof@kernel.org
Subject: [PATCH 2/2] fio: add latency steady state detection
Date: Thu, 24 Jul 2025 23:17:48 -0700	[thread overview]
Message-ID: <20250725061748.2180898-3-mcgrof@kernel.org> (raw)
In-Reply-To: <20250725061748.2180898-1-mcgrof@kernel.org>

Add fio latency steady state support. This is based on the SNIA SSD
Performance Test Specification requirements for latency steady state
detection. The implementation calculates weighted average latency across
all I/O directions and supports both maximum mean deviation and slope-based
detection methods.

Tested successfully against NVMe device with debug output confirming
proper latency calculation and steady state evaluation.

Generated-by: Claude AI
Signed-off-by: Luis Chamberlain <mcgrof@kernel.org>
---
 HOWTO.rst                       | 12 +++++
 client.c                        |  1 +
 example_latency_steadystate.fio | 47 ++++++++++++++++++++
 options.c                       | 26 ++++++++++-
 stat.h                          | 10 +++++
 steadystate.c                   | 78 ++++++++++++++++++++++++++++-----
 steadystate.h                   |  7 +++
 7 files changed, 168 insertions(+), 13 deletions(-)
 create mode 100644 example_latency_steadystate.fio

diff --git a/HOWTO.rst b/HOWTO.rst
index 55ebc388..c5e6a0ad 100644
--- a/HOWTO.rst
+++ b/HOWTO.rst
@@ -4154,6 +4154,18 @@ Steady state
 			Collect bandwidth data and calculate the least squares regression
 			slope. Stop the job if the slope falls below the specified limit.
 
+		**lat**
+			Collect completion latency data and calculate the maximum mean
+			deviation. Stop the job if the deviation falls below the specified
+			limit. The latency values are weighted by the number of I/O samples
+			in each measurement interval.
+
+		**lat_slope**
+			Collect completion latency data and calculate the least squares
+			regression slope. Stop the job if the slope falls below the
+			specified limit. The latency values are weighted by the number
+			of I/O samples in each measurement interval.
+
 .. option:: steadystate_duration=time, ss_dur=time
 
         A rolling window of this duration will be used to judge whether steady
diff --git a/client.c b/client.c
index 923b092e..af37aea1 100644
--- a/client.c
+++ b/client.c
@@ -1077,6 +1077,7 @@ static void convert_ts(struct thread_stat *dst, struct thread_stat *src)
 		for (i = 0; i < dst->ss_dur; i++ ) {
 			dst->ss_iops_data[i] = le64_to_cpu(src->ss_iops_data[i]);
 			dst->ss_bw_data[i] = le64_to_cpu(src->ss_bw_data[i]);
+			dst->ss_lat_data[i] = le64_to_cpu(src->ss_lat_data[i]);
 		}
 	}
 
diff --git a/example_latency_steadystate.fio b/example_latency_steadystate.fio
new file mode 100644
index 00000000..b769ad15
--- /dev/null
+++ b/example_latency_steadystate.fio
@@ -0,0 +1,47 @@
+# Example FIO job file demonstrating latency steady state detection
+# This example shows how to use FIO's latency steady state detection
+# to automatically terminate workloads when latency stabilizes
+#
+# Based on SNIA SSD Performance Test Specification requirements:
+# - Steady state is achieved when latency measurements don't change more than
+#   20% for 5 measurement windows and remain within 5% of a line with 10% slope
+# - This example uses more conservative 5% deviation threshold for demonstration
+
+[global]
+# Basic I/O parameters
+ioengine=libaio
+iodepth=32
+bs=4k
+direct=1
+rw=randread
+numjobs=1
+time_based=1
+runtime=3600  # Max runtime: 1 hour (will terminate early if steady state reached)
+
+# Steady state detection parameters
+steadystate=lat:5%           # Stop when latency mean deviation < 5% of average
+steadystate_duration=300     # Use 5-minute rolling window for measurements
+steadystate_ramp_time=60     # Wait 1 minute before starting measurements
+steadystate_check_interval=10 # Take measurements every 10 seconds
+
+# Output options
+write_lat_log=lat_steadystate
+log_avg_msec=10000           # Log average latency every 10 seconds
+
+[latency_steady_test]
+filename=/dev/nvme3n1
+size=10G
+
+# Alternative steady state configurations (uncomment to try):
+
+# Use slope-based detection instead of deviation:
+# steadystate=lat_slope:0.1%
+
+# More aggressive detection (faster convergence):
+# steadystate=lat:2%
+# steadystate_duration=120    # 2-minute window
+# steadystate_check_interval=5 # Check every 5 seconds
+
+# More conservative detection (slower convergence):
+# steadystate=lat:10%
+# steadystate_duration=600    # 10-minute window
diff --git a/options.c b/options.c
index 6295a616..8884dd8a 100644
--- a/options.c
+++ b/options.c
@@ -1370,7 +1370,8 @@ static int str_steadystate_cb(void *data, const char *str)
 	long long ll;
 
 	if (td->o.ss_state != FIO_SS_IOPS && td->o.ss_state != FIO_SS_IOPS_SLOPE &&
-	    td->o.ss_state != FIO_SS_BW && td->o.ss_state != FIO_SS_BW_SLOPE) {
+	    td->o.ss_state != FIO_SS_BW && td->o.ss_state != FIO_SS_BW_SLOPE &&
+	    td->o.ss_state != FIO_SS_LAT && td->o.ss_state != FIO_SS_LAT_SLOPE) {
 		/* should be impossible to get here */
 		log_err("fio: unknown steady state criterion\n");
 		return 1;
@@ -1414,6 +1415,21 @@ static int str_steadystate_cb(void *data, const char *str)
 			return 0;
 
 		td->o.ss_limit.u.f = val;
+        } else if (td->o.ss_state & FIO_SS_LAT) {
+                long long tns;
+                if (check_str_time(nr, &tns, 0)) {
+                        log_err("fio: steadystate latency threshold parsing failed\n");
+                        free(nr);
+                        return 1;
+                }
+
+                dprint(FD_PARSE, "set steady state latency threshold to %lld nsec\n", tns);
+                free(nr);
+                if (parse_dryrun())
+                        return 0;
+
+                td->o.ss_limit.u.f = (double) tns;
+
 	} else {	/* bandwidth criterion */
 		if (str_to_decimal(nr, &ll, 1, td, 0, 0)) {
 			log_err("fio: steadystate BW threshold postfix parsing failed\n");
@@ -5489,6 +5505,14 @@ struct fio_option fio_options[FIO_MAX_OPTS] = {
 			    .oval = FIO_SS_BW_SLOPE,
 			    .help = "slope calculated from bandwidth measurements",
 			  },
+                          { .ival = "lat",
+                            .oval = FIO_SS_LAT,
+                            .help = "maximum mean deviation of latency measurements",
+                          },
+                          { .ival = "lat_slope",
+                            .oval = FIO_SS_LAT_SLOPE,
+                            .help = "slope calculated from latency measurements",
+                          },
 		},
 		.category = FIO_OPT_C_GENERAL,
 		.group  = FIO_OPT_G_RUNTIME,
diff --git a/stat.h b/stat.h
index ac74d6c2..fad7e8d3 100644
--- a/stat.h
+++ b/stat.h
@@ -282,6 +282,16 @@ struct thread_stat {
 		uint64_t pad5;
 	};
 
+	union {
+		uint64_t *ss_lat_data;
+		/*
+		 * For FIO_NET_CMD_TS, the pointed to data will temporarily
+		 * be stored at this offset from the start of the payload.
+		 */
+		uint64_t ss_lat_data_offset;
+		uint64_t pad5b;
+	};
+
 	union {
 		struct clat_prio_stat *clat_prio[DDIR_RWDIR_CNT];
 		/*
diff --git a/steadystate.c b/steadystate.c
index 3e3683f3..96924b96 100644
--- a/steadystate.c
+++ b/steadystate.c
@@ -10,8 +10,10 @@ void steadystate_free(struct thread_data *td)
 {
 	free(td->ss.iops_data);
 	free(td->ss.bw_data);
+	free(td->ss.lat_data);
 	td->ss.iops_data = NULL;
 	td->ss.bw_data = NULL;
+	td->ss.lat_data = NULL;
 }
 
 static void steadystate_alloc(struct thread_data *td)
@@ -20,6 +22,7 @@ static void steadystate_alloc(struct thread_data *td)
 
 	td->ss.bw_data = calloc(intervals, sizeof(uint64_t));
 	td->ss.iops_data = calloc(intervals, sizeof(uint64_t));
+	td->ss.lat_data = calloc(intervals, sizeof(uint64_t));
 
 	td->ss.state |= FIO_SS_DATA;
 }
@@ -60,7 +63,7 @@ void steadystate_setup(void)
 		steadystate_alloc(prev_td);
 }
 
-static bool steadystate_slope(uint64_t iops, uint64_t bw,
+static bool steadystate_slope(uint64_t iops, uint64_t bw, double lat,
 			      struct thread_data *td)
 {
 	int i, j;
@@ -71,11 +74,14 @@ static bool steadystate_slope(uint64_t iops, uint64_t bw,
 
 	ss->bw_data[ss->tail] = bw;
 	ss->iops_data[ss->tail] = iops;
+	ss->lat_data[ss->tail] = (uint64_t)lat;
 
 	if (ss->state & FIO_SS_IOPS)
 		new_val = iops;
-	else
+	else if (ss->state & FIO_SS_BW)
 		new_val = bw;
+	else
+		new_val = (uint64_t)lat;
 
 	if (ss->state & FIO_SS_BUFFER_FULL || ss->tail - ss->head == intervals - 1) {
 		if (!(ss->state & FIO_SS_BUFFER_FULL)) {
@@ -83,13 +89,17 @@ static bool steadystate_slope(uint64_t iops, uint64_t bw,
 			for (i = 0, ss->sum_y = 0; i < intervals; i++) {
 				if (ss->state & FIO_SS_IOPS)
 					ss->sum_y += ss->iops_data[i];
-				else
+				else if (ss->state & FIO_SS_BW)
 					ss->sum_y += ss->bw_data[i];
+				else
+					ss->sum_y += ss->lat_data[i];
 				j = (ss->head + i) % intervals;
 				if (ss->state & FIO_SS_IOPS)
 					ss->sum_xy += i * ss->iops_data[j];
-				else
+				else if (ss->state & FIO_SS_BW)
 					ss->sum_xy += i * ss->bw_data[j];
+				else
+					ss->sum_xy += i * ss->lat_data[j];
 			}
 			ss->state |= FIO_SS_BUFFER_FULL;
 		} else {		/* easy to update the sums */
@@ -100,8 +110,10 @@ static bool steadystate_slope(uint64_t iops, uint64_t bw,
 
 		if (ss->state & FIO_SS_IOPS)
 			ss->oldest_y = ss->iops_data[ss->head];
-		else
+		else if (ss->state & FIO_SS_BW)
 			ss->oldest_y = ss->bw_data[ss->head];
+		else
+			ss->oldest_y = ss->lat_data[ss->head];
 
 		/*
 		 * calculate slope as (sum_xy - sum_x * sum_y / n) / (sum_(x^2)
@@ -134,7 +146,7 @@ static bool steadystate_slope(uint64_t iops, uint64_t bw,
 	return false;
 }
 
-static bool steadystate_deviation(uint64_t iops, uint64_t bw,
+static bool steadystate_deviation(uint64_t iops, uint64_t bw, double lat,
 				  struct thread_data *td)
 {
 	int i;
@@ -146,6 +158,7 @@ static bool steadystate_deviation(uint64_t iops, uint64_t bw,
 
 	ss->bw_data[ss->tail] = bw;
 	ss->iops_data[ss->tail] = iops;
+	ss->lat_data[ss->tail] = (uint64_t)lat;
 
 	if (ss->state & FIO_SS_BUFFER_FULL || ss->tail - ss->head == intervals  - 1) {
 		if (!(ss->state & FIO_SS_BUFFER_FULL)) {
@@ -153,22 +166,28 @@ static bool steadystate_deviation(uint64_t iops, uint64_t bw,
 			for (i = 0, ss->sum_y = 0; i < intervals; i++) {
 				if (ss->state & FIO_SS_IOPS)
 					ss->sum_y += ss->iops_data[i];
-				else
+				else if (ss->state & FIO_SS_BW)
 					ss->sum_y += ss->bw_data[i];
+				else
+					ss->sum_y += ss->lat_data[i];
 			}
 			ss->state |= FIO_SS_BUFFER_FULL;
 		} else {		/* easy to update the sum */
 			ss->sum_y -= ss->oldest_y;
 			if (ss->state & FIO_SS_IOPS)
 				ss->sum_y += ss->iops_data[ss->tail];
-			else
+			else if (ss->state & FIO_SS_BW)
 				ss->sum_y += ss->bw_data[ss->tail];
+			else
+				ss->sum_y += ss->lat_data[ss->tail];
 		}
 
 		if (ss->state & FIO_SS_IOPS)
 			ss->oldest_y = ss->iops_data[ss->head];
-		else
+		else if (ss->state & FIO_SS_BW)
 			ss->oldest_y = ss->bw_data[ss->head];
+		else
+			ss->oldest_y = ss->lat_data[ss->head];
 
 		mean = (double) ss->sum_y / intervals;
 		ss->deviation = 0.0;
@@ -176,8 +195,10 @@ static bool steadystate_deviation(uint64_t iops, uint64_t bw,
 		for (i = 0; i < intervals; i++) {
 			if (ss->state & FIO_SS_IOPS)
 				diff = ss->iops_data[i] - mean;
-			else
+			else if (ss->state & FIO_SS_BW)
 				diff = ss->bw_data[i] - mean;
+			else
+				diff = ss->lat_data[i] - mean;
 			ss->deviation = max(ss->deviation, diff * (diff < 0.0 ? -1.0 : 1.0));
 		}
 
@@ -209,13 +230,18 @@ int steadystate_check(void)
 	unsigned long rate_time;
 	struct timespec now;
 	uint64_t group_bw = 0, group_iops = 0;
+	double group_lat_sum = 0.0;
+	uint64_t group_lat_samples = 0;
 	uint64_t td_iops, td_bytes;
+	double group_lat;
 	bool ret;
 
 	prev_groupid = -1;
 	for_each_td(td) {
 		const bool needs_lock = td_async_processing(td);
 		struct steadystate_data *ss = &td->ss;
+		double td_lat_sum = 0.0;
+		uint64_t td_lat_samples = 0;
 
 		if (!ss->dur || td->runstate <= TD_SETTING_UP ||
 		    td->runstate >= TD_EXITED || !ss->state ||
@@ -228,6 +254,8 @@ int steadystate_check(void)
 		    (td->o.group_reporting && td->groupid != prev_groupid)) {
 			group_bw = 0;
 			group_iops = 0;
+			group_lat_sum = 0.0;
+			group_lat_samples = 0;
 			group_ramp_time_over = 0;
 		}
 		prev_groupid = td->groupid;
@@ -248,6 +276,9 @@ int steadystate_check(void)
 		for (ddir = 0; ddir < DDIR_RWDIR_CNT; ddir++) {
 			td_iops += td->io_blocks[ddir];
 			td_bytes += td->io_bytes[ddir];
+			td_lat_sum += td->ts.clat_stat[ddir].mean.u.f *
+				      td->ts.clat_stat[ddir].samples;
+			td_lat_samples += td->ts.clat_stat[ddir].samples;
 		}
 
 		if (needs_lock)
@@ -261,10 +292,14 @@ int steadystate_check(void)
 				(ss_check_interval * ss_check_interval / 1000L);
 			group_iops += rate_time * (td_iops - ss->prev_iops) /
 				(ss_check_interval * ss_check_interval / 1000L);
+			group_lat_sum += td_lat_sum - ss->prev_lat_sum;
+			group_lat_samples += td_lat_samples - ss->prev_lat_samples;
 			++group_ramp_time_over;
 		}
 		ss->prev_iops = td_iops;
 		ss->prev_bytes = td_bytes;
+		ss->prev_lat_sum = td_lat_sum;
+		ss->prev_lat_samples = td_lat_samples;
 
 		if (td->o.group_reporting && !(ss->state & FIO_SS_DATA))
 			continue;
@@ -284,10 +319,14 @@ int steadystate_check(void)
 					(unsigned long long) group_bw,
 					ss->head, ss->tail);
 
+		group_lat = 0.0;
+		if (group_lat_samples)
+			group_lat = group_lat_sum / group_lat_samples;
+
 		if (ss->state & FIO_SS_SLOPE)
-			ret = steadystate_slope(group_iops, group_bw, td);
+			ret = steadystate_slope(group_iops, group_bw, group_lat, td);
 		else
-			ret = steadystate_deviation(group_iops, group_bw, td);
+			ret = steadystate_deviation(group_iops, group_bw, group_lat, td);
 
 		if (ret) {
 			if (td->o.group_reporting) {
@@ -382,3 +421,18 @@ uint64_t steadystate_iops_mean(struct thread_stat *ts)
 
 	return sum / intervals;
 }
+
+uint64_t steadystate_lat_mean(struct thread_stat *ts)
+{
+	int i;
+	uint64_t sum;
+	int intervals = ts->ss_dur / (ss_check_interval / 1000L);
+
+	if (!ts->ss_dur)
+		return 0;
+
+	for (i = 0, sum = 0; i < intervals; i++)
+		sum += ts->ss_lat_data[i];
+
+	return sum / intervals;
+}
diff --git a/steadystate.h b/steadystate.h
index f1ef2b20..fffcb463 100644
--- a/steadystate.h
+++ b/steadystate.h
@@ -9,6 +9,7 @@ extern void steadystate_setup(void);
 extern int td_steadystate_init(struct thread_data *);
 extern uint64_t steadystate_bw_mean(struct thread_stat *);
 extern uint64_t steadystate_iops_mean(struct thread_stat *);
+extern uint64_t steadystate_lat_mean(struct thread_stat *);
 
 extern bool steadystate_enabled;
 extern unsigned int ss_check_interval;
@@ -24,6 +25,7 @@ struct steadystate_data {
 	unsigned int tail;
 	uint64_t *iops_data;
 	uint64_t *bw_data;
+	uint64_t *lat_data;
 
 	double slope;
 	double deviation;
@@ -38,6 +40,8 @@ struct steadystate_data {
 	struct timespec prev_time;
 	uint64_t prev_iops;
 	uint64_t prev_bytes;
+	double prev_lat_sum;
+	uint64_t prev_lat_samples;
 };
 
 enum {
@@ -49,6 +53,7 @@ enum {
 	__FIO_SS_DATA,
 	__FIO_SS_PCT,
 	__FIO_SS_BUFFER_FULL,
+	__FIO_SS_LAT,
 };
 
 enum {
@@ -60,9 +65,11 @@ enum {
 	FIO_SS_DATA		= 1 << __FIO_SS_DATA,
 	FIO_SS_PCT		= 1 << __FIO_SS_PCT,
 	FIO_SS_BUFFER_FULL	= 1 << __FIO_SS_BUFFER_FULL,
+	FIO_SS_LAT		= 1 << __FIO_SS_LAT,
 
 	FIO_SS_IOPS_SLOPE	= FIO_SS_IOPS | FIO_SS_SLOPE,
 	FIO_SS_BW_SLOPE		= FIO_SS_BW | FIO_SS_SLOPE,
+	FIO_SS_LAT_SLOPE	= FIO_SS_LAT | FIO_SS_SLOPE,
 };
 
 #endif
-- 
2.47.2


  parent reply	other threads:[~2025-07-25  6:17 UTC|newest]

Thread overview: 6+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2025-07-25  6:17 [PATCH 0/2] fio: steady state for latency Luis Chamberlain
2025-07-25  6:17 ` [PATCH 1/2] configure: libnfs + gnutls Luis Chamberlain
2025-07-25 17:44   ` Vincent Fu
2025-07-25  6:17 ` Luis Chamberlain [this message]
2025-07-28 18:14   ` [PATCH 2/2] fio: add latency steady state detection Vincent Fu
2025-07-28 21:27   ` Sitsofe Wheeler

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=20250725061748.2180898-3-mcgrof@kernel.org \
    --to=mcgrof@kernel.org \
    --cc=fio@vger.kernel.org \
    --cc=vincent.fu@samsung.com \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.