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
next prev 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 a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox