#!/bin/bash
# SPDX-License-Identifier: GPL-3.0+
# Copyright (C) 2017 Omar Sandoval
#
# fio helper functions.

. common/shellcheck

_have_fio() {
	if ! _have_program fio; then
		return 1
	fi
	if ! fio --parse-only --terse-version=4 >/dev/null 2>&1; then
		SKIP_REASONS+=("Fio version too old (does not support --terse-version=4)")
		return 1
	fi
	return 0
}

_have_fio_zbd_zonemode() {
	_have_fio || return $?
	if ! fio --cmdhelp=zonemode 2>&1 | grep -q zbd; then
		SKIP_REASONS+=("Fio version too old (does not support --zonemode=zbd)")
		return 1
	fi
	return 0
}

# Check whether the version of the fio is greater than or equal to $1.$2.$3
_have_fio_ver() {
	local d=$1 e=$2 f=$3

	_have_fio || return $?

	IFS='.' read -r a b c < <(fio --version | cut -c 5- | sed 's/-.*//')
	if [ $((a * 65536 + b * 256 + c)) -lt $((d * 65536 + e * 256 + f)) ];
	then
		SKIP_REASONS+=("fio version too old")
		return 1
	fi
	return 0
}

declare -A FIO_TERSE_FIELDS
FIO_TERSE_FIELDS=(
	# Read status
	["read io"]=6
	["read bandwidth"]=7
	["read iops"]=8
	["read runtime"]=9
	["read slat min"]=10
	["read slat max"]=11
	["read slat mean"]=12
	["read slat stdev"]=13
	["read clat min"]=14
	["read clat max"]=15
	["read clat mean"]=16
	["read clat stdev"]=17
	# read clat percentiles are 18-37
	["read lat min"]=38
	["read lat max"]=39
	["read lat mean"]=40
	["read lat stdev"]=41
	["read bandwidth min"]=42
	["read bandwidth max"]=43
	["read bandwidth %"]=44
	["read bandwidth mean"]=45
	["read bandwidth stdev"]=46

	# Write status
	["write io"]=47
	["write bandwidth"]=48
	["write iops"]=49
	["write runtime"]=50
	["write slat min"]=51
	["write slat max"]=52
	["write slat mean"]=53
	["write slat stdev"]=54
	["write clat min"]=55
	["write clat max"]=56
	["write clat mean"]=57
	["write clat stdev"]=58
	# write clat percentiles are 59-78
	["write lat min"]=79
	["write lat max"]=80
	["write lat mean"]=81
	["write lat stdev"]=82
	["write bandwidth min"]=83
	["write bandwidth max"]=84
	["write bandwidth %"]=85
	["write bandwidth mean"]=86
	["write bandwidth stdev"]=87

	# Trim status
	["trim io"]=88
	["trim bandwidth"]=89
	["trim iops"]=90
	["trim runtime"]=91
	["trim slat min"]=92
	["trim slat max"]=93
	["trim slat mean"]=94
	["trim slat stdev"]=95
	["trim clat min"]=96
	["trim clat max"]=97
	["trim clat mean"]=98
	["trim clat stdev"]=99
	# trim clat percentiles are 100-119
	["trim lat min"]=120
	["trim lat max"]=121
	["trim lat mean"]=122
	["trim lat stdev"]=123
	["trim bandwidth min"]=124
	["trim bandwidth max"]=125
	["trim bandwidth %"]=126
	["trim bandwidth mean"]=127
	["trim bandwidth stdev"]=128

	# CPU usage
	["user cpu"]=129
	["system cpu"]=130
	["context switches"]=131
	["major page faults"]=132
	["minor page faults"]=133

	# IO depth distribution
	["io depth <=1"]=134
	["io depth 2"]=135
	["io depth 4"]=136
	["io depth 8"]=137
	["io depth 16"]=138
	["io depth 32"]=139
	["io depth >=64"]=140

	# IO latency distribution
	["io latency <=2 us"]=141
	["io latency 4 us"]=142
	["io latency 10 us"]=143
	["io latency 20 us"]=144
	["io latency 50 us"]=145
	["io latency 100 us"]=146
	["io latency 250 us"]=147
	["io latency 500 us"]=148
	["io latency 750 us"]=149
	["io latency 1000 us"]=150
	["io latency <=2 ms"]=151
	["io latency 4 ms"]=152
	["io latency 10 ms"]=153
	["io latency 20 ms"]=154
	["io latency 50 ms"]=155
	["io latency 100 ms"]=156
	["io latency 250 ms"]=157
	["io latency 500 ms"]=158
	["io latency 750 ms"]=159
	["io latency 1000 ms"]=160
	["io latency 2000 ms"]=161
	["io latency >=2000 ms"]=162

	# Disk utilization (11 fields per disk)
)

# Run fio and report performance data. The metrics to gather are specified by
# the $FIO_PERF_FIELDS array. E.g., FIO_PERF_FIELDS=("read iops" "system cpu").
# The possible fields are specified above. The optional $FIO_PERF_PREFIX
# variable is prepended to the field name when reporting.
_fio_perf() {
	_run_fio "$@"
	_fio_perf_report
}

# Wrapper around fio that handles:
#     - Recording perf results
#     - $TIMEOUT
# You should usually use this instead of calling fio directly. An explicitly
# passed --runtime will override the configured $TIMEOUT, which is useful for
# tests that should run for a specific amount of time.
_run_fio() {
	local args=("--output=$TMPDIR/fio_perf" "--output-format=terse" "--terse-version=4" "--group_reporting=1")

	if [[ "${TIMEOUT:-}" ]]; then
		args+=("--runtime=$TIMEOUT")
	fi

	if ! fio "${args[@]}" "$@"; then
		echo "fio exited with status $?"
		cat "$TMPDIR"/fio_perf
	fi
}

# Wrapper around _run_fio used if you need some I/O but don't really care much
# about the details
_run_fio_rand_io() {
	_run_fio --bs=4k --rw=randread --norandommap --numjobs="$(nproc)" \
		--name=reads --direct=1 "$@"
}

_run_fio_verify_io() {
	_run_fio --name=verify --rw=randwrite --direct=1 --ioengine=libaio --bs=4k \
		--iodepth=16 --verify=crc32c "$@"
	rm -f local*verify*state
}

_fio_perf_report() {
	# If there is more than one group, we don't know what to report.
	if [[ $(wc -l < "$TMPDIR/fio_perf") -gt 1 ]]; then
		echo "_fio_perf: too many terse lines" >&2
		return
	fi

	local name field value
	for name in "${FIO_PERF_FIELDS[@]}"; do
		field="${FIO_TERSE_FIELDS["$name"]}"
		if [[ -z $field ]]; then
			echo "_fio_perf: unknown fio terse field '$name'" >&2
			continue
		fi
		value="$(cut -d ';' -f "$field" "$TMPDIR/fio_perf")"
		TEST_RUN["$FIO_PERF_PREFIX$name"]="$value"
	done
}
