diff --git a/docs/benchmark-transfer-options.sh b/docs/benchmark-transfer-options.sh index 284ca3e..8e27bbb 100755 --- a/docs/benchmark-transfer-options.sh +++ b/docs/benchmark-transfer-options.sh @@ -10,23 +10,29 @@ # # Takes a path to a pv binary as an argument. -pv="$1" -test -n "${pv}" || pv='pv' - +# Defaults. +pv='pv' # pv executable to run the measurements with rounds='10' # how many rounds of measurements to take testFileMB='256' # max size of each of the test files, in MiB testZeroesMB='1024' # amount of /dev/zero data to use, in MiB -# Use /dev/shm for workspace if possible to eliminate disk I/O as a factor. -if test -d /dev/shm && mountpoint -q /dev/shm; then - export TMPDIR=/dev/shm -fi -# Check there's enough room for the test files - make them smaller, if not. -tmpSpaceMB="$(df -kP "${TMPDIR:-/tmp}" | awk 'FNR==2 {print int($4/1024)}')" -while test ${testFileMB} -gt 4; do - test "${tmpSpaceMB}" -gt $((2+3*testFileMB)) && break - testFileMB=$((testFileMB/2)) -done +programName='benchmark-pv-transfers' +programVersion='0.1.0' +bugReportsTo='https://codeberg.org/ivarch/pv/issues' +copyrightYear='2026' +copyrightHolder='Andrew Wood' + +# Write an error message $1 to standard error, prefixed with the program +# name. +error () { + printf '%s: %s\n' "${programName}" "$1" >&2 +} + +# Output an error message $1, and exit with a failure status. +die () { + error "$1" + exit 1 +} # Write an output line of up to 7 arguments, prefixed with a system ID and # the current time. @@ -79,72 +85,46 @@ sys %3S" fi } -# Temporary working area, cleaned up on exit. -workDir="$(mktemp -d)" || exit 1 -trap 'rm -rf "${workDir}"' EXIT - -# Set everything to the "C" locale. -LANG=C -LC_ALL=C -export LANG LC_ALL - -# Capture the help text so that capabilities can be checked. -${pv} -h > "${workDir}/help" - -# Basic system information, and a header line for the test results. -outputLine 'System hostname' "$(uname -n)" -outputLine 'System load' "$(uptime | awk '{printf "%.2f\n",$(NF-2)}')" -outputLine 'System kernel type' "$(uname -s)" -outputLine 'System kernel release' "$(uname -r)" -outputLine 'System OS' "$(uname -o)" -outputLine 'PV path' "${pv}" -outputLine 'PV version' "$(${pv} -V | awk 'FNR==1 {print $2}')" -outputLine 'Test file size (MB)' "${testFileMB}" - -# Generate two files of random data. -dd if='/dev/urandom' of="${workDir}/file1" bs=1048576 count="${testFileMB}" 2>/dev/null -dd if='/dev/urandom' of="${workDir}/file2" bs=1048576 count="${testFileMB}" 2>/dev/null - # Run various of transfer types with extra options "$2", naming them "$1". runTransfers () { # From file via stdin to file via stdout. rm -f "${workDir}/stdout" - captureTimes $((testFileMB)) "${pv} -q $2 < ${workDir}/file1 > ${workDir}/stdout" + captureTimes $((testFileMB)) "${pv} $2 < ${workDir}/file1 > ${workDir}/stdout" resultsLine "$1: stdin file to file" # From file to file via stdout. rm -f "${workDir}/stdout" - captureTimes $((testFileMB)) "${pv} -q $2 ${workDir}/file1 > ${workDir}/stdout" + captureTimes $((testFileMB)) "${pv} $2 ${workDir}/file1 > ${workDir}/stdout" resultsLine "$1: file to file" # From two files to file via stdout. rm -f "${workDir}/stdout" - captureTimes $((2*testFileMB)) "${pv} -q $2 ${workDir}/file1 ${workDir}/file2 > ${workDir}/stdout" + captureTimes $((2*testFileMB)) "${pv} $2 ${workDir}/file1 ${workDir}/file2 > ${workDir}/stdout" resultsLine "$1: two files to file" # From pipe to file via stdout. rm -f "${workDir}/stdout" - captureTimes $((testFileMB)) "cat ${workDir}/file1 | ${pv} -q $2 > ${workDir}/stdout" + captureTimes $((testFileMB)) "cat ${workDir}/file1 | ${pv} $2 > ${workDir}/stdout" resultsLine "$1: pipe to file" # From file via stdin to pipe. rm -f "${workDir}/stdout" - captureTimes $((testFileMB)) "${pv} -q $2 < ${workDir}/file1 | cat > ${workDir}/stdout" + captureTimes $((testFileMB)) "${pv} $2 < ${workDir}/file1 | cat > ${workDir}/stdout" resultsLine "$1: stdin file to pipe" # From file to pipe. rm -f "${workDir}/stdout" - captureTimes $((testFileMB)) "${pv} -q $2 ${workDir}/file1 | cat > ${workDir}/stdout" + captureTimes $((testFileMB)) "${pv} $2 ${workDir}/file1 | cat > ${workDir}/stdout" resultsLine "$1: file to pipe" # From two files to pipe. rm -f "${workDir}/stdout" - captureTimes $((2*testFileMB)) "${pv} -q $2 ${workDir}/file1 ${workDir}/file2 | cat > ${workDir}/stdout" + captureTimes $((2*testFileMB)) "${pv} $2 ${workDir}/file1 ${workDir}/file2 | cat > ${workDir}/stdout" resultsLine "$1: two files to pipe" # From pipe to pipe. rm -f "${workDir}/stdout" - captureTimes $((testFileMB)) "cat ${workDir}/file1 | ${pv} -q $2 | cat > ${workDir}/stdout" + captureTimes $((testFileMB)) "cat ${workDir}/file1 | ${pv} $2 | cat > ${workDir}/stdout" resultsLine "$1: pipe to pipe" } @@ -170,64 +150,178 @@ gatherMeasurements () { fi if grep -Fq ' -S' "${workDir}/help"; then - captureTimes $((testZeroesMB)) "${pv} -q -Ss ${testZeroesMB}M /dev/zero > /dev/null" + captureTimes $((testZeroesMB)) "${pv} -Ss ${testZeroesMB}M /dev/zero > /dev/null" resultsLine "Zeroes: stdout to /dev/null" - captureTimes $((testZeroesMB)) "${pv} -q -Ss ${testZeroesMB}M /dev/zero | cat > /dev/null" + captureTimes $((testZeroesMB)) "${pv} -Ss ${testZeroesMB}M /dev/zero | cat > /dev/null" resultsLine "Zeroes: stdout to pipe" if grep -Fq ' -X' "${workDir}/help"; then - captureTimes $((testZeroesMB)) "${pv} -q -X -Ss ${testZeroesMB}M /dev/zero" + captureTimes $((testZeroesMB)) "${pv} -X -Ss ${testZeroesMB}M /dev/zero" resultsLine "Zeroes: discarded" fi if grep -Fq ' -C' "${workDir}/help"; then - captureTimes $((testZeroesMB)) "${pv} -q -C -Ss ${testZeroesMB}M /dev/zero > /dev/null" + captureTimes $((testZeroesMB)) "${pv} -C -Ss ${testZeroesMB}M /dev/zero > /dev/null" resultsLine "Zeroes: stdout to /dev/null with no-splice" - captureTimes $((testZeroesMB)) "${pv} -q -C -Ss ${testZeroesMB}M /dev/zero | cat > /dev/null" + captureTimes $((testZeroesMB)) "${pv} -C -Ss ${testZeroesMB}M /dev/zero | cat > /dev/null" resultsLine "Zeroes: stdout to pipe with no-splice" fi if grep -Fq ' -C' "${workDir}/help" && grep -Fq ' -X' "${workDir}/help"; then - captureTimes $((testZeroesMB)) "${pv} -q -C -X -Ss ${testZeroesMB}M /dev/zero" + captureTimes $((testZeroesMB)) "${pv} -C -X -Ss ${testZeroesMB}M /dev/zero" resultsLine "Zeroes: discarded with no-splice" fi fi } -# Run several rounds of measurements. -outputLine '#' 'ID' 'MiB/sec' 'Real time' 'User CPU time' 'System CPU time' 'Raw measurement' -thisRound=0 -while test ${thisRound} -lt ${rounds}; do - thisRound=$((1+thisRound)) - gatherMeasurements -done +# Run several rounds of benchmark measurements using $1 as the pv +# executable, producing tab-separated data, including a final section +# containing the means and standard deviations for each measurement type. +# +runBenchmarks () { + pv="$1" -# For each of the types of measurement, report the mean and standard -# deviation of each field. -awk -F "\t" '{print $1}' < "${workDir}/results" > "${workDir}/measurement-types" -true > "${workDir}/measurement-types-used" -outputLine 'μ/σ' 'ID' 'MiB/sec' 'Real time' 'User CPU time' 'System CPU time' 'Aggregated measurement' -{ -while read -r measurement; do - # Skip this type of measurement if already processed. - grep -Fqx "${measurement}" "${workDir}/measurement-types-used" && continue - printf '%s\n' "${measurement}" >> "${workDir}/measurement-types-used" - # Hash the measurement name. - measurementHash="$(printf '%s\n' "${measurement}" | md5sum | cut -b1-7)" - # Separate out this measurement type's results. - awk -F "\t" -v "m=${measurement}" '$1==m {print}' < "${workDir}/results" \ - > "${workDir}/measurements" - # Calculate the mean of each field. - awk -F "\t" -v "h=${measurementHash}" -v fieldcount=5 \ + # Check there's enough room for the test files - make them smaller, + # if not. + tmpSpaceMB="$(df -kP "${TMPDIR:-/tmp}" | awk 'FNR==2 {print int($4/1024)}')" + while test ${testFileMB} -gt 4; do + test "${tmpSpaceMB}" -gt $((2+3*testFileMB)) && break + testFileMB=$((testFileMB/2)) + done + + # Capture the help text so that capabilities can be checked. + ${pv} -h > "${workDir}/help" + + # Basic system information, and a header line for the test results. + outputLine 'System hostname' "$(uname -n)" + outputLine 'System load' "$(uptime | awk '{printf "%.2f\n",$(NF-2)}')" + outputLine 'System kernel type' "$(uname -s)" + outputLine 'System kernel release' "$(uname -r)" + outputLine 'System OS' "$(uname -o)" + outputLine 'PV path' "${pv}" + outputLine 'PV version' "$(${pv} -V | awk 'FNR==1 {print $2}')" + outputLine 'Test file size (MB)' "${testFileMB}" + + # Generate two files of random data. + dd if='/dev/urandom' of="${workDir}/file1" bs=1048576 count="${testFileMB}" 2>/dev/null + dd if='/dev/urandom' of="${workDir}/file2" bs=1048576 count="${testFileMB}" 2>/dev/null + + # Run several rounds of measurements. + outputLine '#' 'ID' 'MiB/sec' 'Real time' 'User CPU time' 'System CPU time' 'Raw measurement' + thisRound=0 + while test ${thisRound} -lt "${rounds}"; do + thisRound=$((1+thisRound)) + gatherMeasurements + done + + # For each of the types of measurement, report the mean and standard + # deviation of each field. + awk -F "\t" '{print $1}' < "${workDir}/results" > "${workDir}/measurement-types" + true > "${workDir}/measurement-types-used" + outputLine 'μ/σ' 'ID' 'MiB/sec' 'Real time' 'User CPU time' 'System CPU time' 'Aggregated measurement' + { + while read -r measurement; do + # Skip this type of measurement if already processed. + grep -Fqx "${measurement}" "${workDir}/measurement-types-used" && continue + printf '%s\n' "${measurement}" >> "${workDir}/measurement-types-used" + # Hash the measurement name. + measurementHash="$(printf '%s\n' "${measurement}" | md5sum | cut -b1-7)" + # Separate out this measurement type's results. + awk -F "\t" -v "m=${measurement}" '$1==m {print}' < "${workDir}/results" \ + > "${workDir}/measurements" + # Calculate the mean of each field. + awk -F "\t" -v "h=${measurementHash}" -v fieldcount=5 \ 'BEGIN { samples=0 } { m=$1; samples++; for (field=1; field<=fieldcount; field++) { total[field] += $(1+field) } } END { printf "%s\t%s", "μ", h; for (field=1; field<=fieldcount; field++) { printf "\t%.3f", total[field]/samples }; printf "\t%s\n", m }' \ - < "${workDir}/measurements" > "${workDir}/mean" - # Calculate the standard deviation of each field. - cat "${workDir}/mean" "${workDir}/measurements" \ - | awk -F "\t" -v "h=${measurementHash}" -v "m=${measurement}" -v fieldcount=5 \ + < "${workDir}/measurements" > "${workDir}/mean" + # Calculate the standard deviation of each field. + cat "${workDir}/mean" "${workDir}/measurements" \ + | awk -F "\t" -v "h=${measurementHash}" -v "m=${measurement}" -v fieldcount=5 \ 'BEGIN { samples=0 } FNR==1 { for (field=1; field<=fieldcount; field++) { mean[field] += $(2+field) } } FNR>1 { samples++; for (field=1; field<=fieldcount; field++) { variance=$(1+field)-mean[field]; sum_variance_squared[field] += (variance*variance) } } END { printf "%s\t%s", "σ", h; for (field=1; field<=fieldcount; field++) { printf "\t%.3f", sqrt(sum_variance_squared[field]/samples) }; printf "\t%s\n", m }' \ - > "${workDir}/stddev" - sed "s!^!${sysId}\t${pvId}\t${runId}\t!" "${workDir}/mean" "${workDir}/stddev" + > "${workDir}/stddev" + sed "s!^!${sysId}\t${pvId}\t${runId}\t!" "${workDir}/mean" "${workDir}/stddev" + done + } < "${workDir}/measurement-types" +} + + +############################################################################## +# Main entry point. + +# Process any command-line options. +action='benchmark' +while test -n "$1"; do + arg="$1" + shift + case "${arg}" in + 'benchmark'|'analyse') action="${arg}" ;; + '-h'|'--help') + cat - < +This is free software: you are free to change and redistribute it. +There is NO WARRANTY, to the extent permitted by law. +EOF + exit 0 + ;; + '-p'|'--program'|'--pv') pv="$1"; test $# -gt 0 && shift ;; + '--program='*|'--pv='*) pv="${arg#*=}" ;; + '-r'|'--rounds') rounds="$1"; test $# -gt 0 && shift ;; + '--rounds='*) rounds="${arg#*=}" ;; + '-s'|'--size') testFileMB="$1"; test $# -gt 0 && shift ;; + '--size='*) testFileMB="${arg#*=}" ;; + '-z'|'--zeroes') testZeroesMB="$1"; test $# -gt 0 && shift ;; + '--zeroes='*) testZeroesMB="${arg#*=}" ;; + '-'*) die "${arg}: unknown option - try \`--help'" ;; + *) die "${arg}: unexpected argument - try \`--help'" ;; + esac done -} < "${workDir}/measurement-types" + +# Use /dev/shm for workspace if possible, to eliminate disk I/O as a factor. +if test -z "${TMPDIR}" && test -d '/dev/shm' && mountpoint -q '/dev/shm'; then + TMPDIR='/dev/shm' + export TMPDIR +fi + +# Temporary working area, cleaned up on exit. +workDir="$(mktemp -d)" || exit 1 +trap 'rm -rf "${workDir}"' EXIT + +# Set everything to the "C" locale. +LANG=C +LC_ALL=C +export LANG LC_ALL + +# Run the selected action. +case "${action}" in +'benchmark') runBenchmarks "${pv}" ;; +'analyse') die 'TODO' ;; +esac