|
#!/usr/bin/env bash |
|
# shellcheck disable=SC2034,SC2296,SC2162 |
|
|
|
# Posix compliant way to check if a command exists, |
|
# works in bash and zsh, and is used in this script |
|
command_exists() { command -v "${@}" > /dev/null 2>&1; } |
|
|
|
# I'm avoiding subshells in bash and zsh for performance reasons |
|
\shopt -s lastpipe 2> /dev/null |
|
reenable_lastpipe() { \shopt -u lastpipe 2> /dev/null; } |
|
trap reenable_lastpipe EXIT |
|
|
|
# a newline character |
|
newline=$'\n' |
|
|
|
# Some colors for pretty printing |
|
cyan=$'\e[36m' |
|
grey=$'\e[90m' |
|
yellow=$'\e[33m' |
|
color_reset=$'\e[0m' |
|
|
|
# The directory to store benchmark reports |
|
BENCHMARK_REPORT_DIR="${BENCHMARK_REPORT_DIR:-${HOME}/benchmarks}" |
|
|
|
# Pretty print a KV pair as arguments |
|
command_exists print_pair || print_pair() { printf '%s%s%s:%s %s%s\n' "${cyan}" "${1}" "${grey}" "${yellow}" "${2}" "${color_reset}"; } |
|
|
|
# Pretty print a colon seperated KV pair from std input |
|
command_exists pipe_pair || pipe_pair() { sed -E 's/^([[:alnum:]_ "'"'"'-]+):(.*)$/'$'\e[36m''\1:'$'\e[33m''\2'$'\e[0m''\n/'; } |
|
|
|
# Indent a newline separated string |
|
command_exists indent_newline || indent_newline() { fold -w60 -s | pr -to "${1:-17}" | sed '1s/^ *//'; } |
|
|
|
if ! command_exists logformat_log; then |
|
# create a templated timestamped formatted log message |
|
logformat_log() { |
|
local l_level="${1}" |
|
shift |
|
local msg="$(pipe_pair <<< "${*//\\n/${newline}}")" |
|
date '+%H:%M:%S' | read -r l_ts |
|
printf '%8.8s [%5.5s] %s\n' "${l_ts}" "${l_level}" "${msg}" | indent_newline |
|
} |
|
fi |
|
# log a message to stdout |
|
command_exists info_log || info_log() { logformat_log "INFO" "${*}"; } |
|
# log a message to stderr |
|
command_exists error_log || error_log() { logformat_log "ERROR" "${*}" 1>&2; } |
|
|
|
# Check that the benchmark utility is installed |
|
if ! command_exists hyperfine && command_exists brew; then |
|
info_log "Installing: hyperfine" |
|
if brew install hyperfine > /dev/null 2>&1; then |
|
info_log "hyperfine installed" |
|
else |
|
error_log "hyperfine failed to automatically install using 'brew install hyperfine'" |
|
fi |
|
fi |
|
|
|
# Announce how to install the benchmark utility if it's not installed |
|
if ! command_exists hyperfine; then |
|
printf 'hyperfine is not installed, please install it with your package manager\n' |
|
printf 'https://github.com/sharkdp/hyperfine#installation\n\n' |
|
exit 1 |
|
fi |
|
|
|
# track each run of this script with an incrementing run id |
|
run_id() { |
|
local BENCHMARK_REPORT_DIR="${1:-${HOME}/benchmarks}" |
|
[[ -d ${BENCHMARK_REPORT_DIR} ]] || mkdir -p "${BENCHMARK_REPORT_DIR}" |
|
local id_file_path="${BENCHMARK_REPORT_DIR}/.runId" |
|
local RUN_ID |
|
if [[ -f ${id_file_path} ]]; then |
|
read -r RUN_ID < "${id_file_path}" |
|
fi |
|
# If the file is empty, set it to 0 |
|
: "${RUN_ID:=0}" |
|
# Increment the run id |
|
((RUN_ID += 1)) |
|
# Write the new run id to the file, and stdout |
|
printf '%s' "${RUN_ID}" | tee "${BENCHMARK_REPORT_DIR}/.runId" |
|
} |
|
|
|
# Print first line of the shell version output |
|
shelli() { "${@}" --version 2>&1 | head -n 1; } |
|
|
|
# clean, as in, no profile, or rc files to slow down startup times |
|
clean_shell_command() { |
|
local sn="$1" |
|
case "${sn}" in |
|
*"bash") printf '%s' "${sn} --norc --noprofile" ;; |
|
*"zsh") printf '%s' "${sn} -fd" ;; |
|
*) printf '%s' "${sn}" ;; |
|
esac |
|
} |
|
|
|
# Get the shell name and version for labeling the report |
|
shell_command_report_name() { |
|
local sn="$1" |
|
local runId="$2" |
|
local version |
|
|
|
case "${sn}" in |
|
*"bash") ${sn} -c 'echo ${BASH_VERSION}' | read -r version ;; |
|
*"zsh") ${sn} -c 'echo ${ZSH_VERSION}' | read -r version ;; |
|
*) exit 1 ;; |
|
esac |
|
printf '%s' "${sn##*/}-${version}-run${runId}" |
|
} |
|
|
|
main() { |
|
local BENCHMARK_REPORT_DIR="${BENCHMARK_REPORT_DIR:-${HOME}/benchmarks}" |
|
[[ -d ${BENCHMARK_REPORT_DIR} ]] || mkdir -p "${BENCHMARK_REPORT_DIR}" |
|
# Read in the output of run_id() and set it to RUN_ID without using a subshell |
|
# works in bash and zsh |
|
\shopt -s lastpipe 2> /dev/null |
|
run_id "${BENCHMARK_REPORT_DIR:-}" | read -r RUN_ID |
|
uname -v | read -r SYSTEM_INFO |
|
|
|
info_log "This is run number: ${RUN_ID}\nSystem info: ${SYSTEM_INFO}\nBenchmarking functions to check if a command or function exists already\n" |
|
|
|
test_with_shell() { |
|
local sn="${1}" |
|
local test_negative_result="${2:-false}" |
|
if [[ -z ${sn} ]]; then |
|
printf 'No shell provided to test_with_shell()\n' |
|
return 1 |
|
fi |
|
|
|
clean_shell_command "${sn}" | read -r CLEAN_SHELL_COMMAND |
|
info_log "Shell command: ${CLEAN_SHELL_COMMAND}" |
|
|
|
local prefix |
|
if [[ ${test_negative_result} == "false" ]]; then |
|
prefix="positive_result-" |
|
hf_params+=("--prepare" "ef() { return 0; };") |
|
else |
|
# Allow Failures |
|
hf_params+=("-i") |
|
prefix="negative_result-" |
|
fi |
|
local report_name="${prefix:-}$(shell_command_report_name "${sn}" "${RUN_ID}")" |
|
local hf_params=( |
|
"--export-markdown=${BENCHMARK_REPORT_DIR}/${report_name}.md" |
|
"--warmup=10" |
|
"--shell=${CLEAN_SHELL_COMMAND}" |
|
) |
|
if [[ -n ${DEBUG+x} ]]; then |
|
if [[ ${DEBUG} == "stdout" ]]; then |
|
hf_params+=("--show-output") |
|
else |
|
hf_params+=("--output=${BENCHMARK_REPORT_DIR}/${report_name}-output.txt") |
|
fi |
|
fi |
|
info_log "Benchmarking within: $(shelli "${sn}")" |
|
|
|
tests_to_run=( |
|
"-n" "${report_name} type" "ef() { return 0; }; type ef" |
|
"-n" "${report_name} command" "ef() { return 0; }; command -v ef" |
|
"-n" "${report_name} which" "ef() { return 0; }; which ef" |
|
) |
|
|
|
if [[ -n ${TEST_SHELL_FUNCTIONS_ONLY+x} ]]; then |
|
tests_to_run+=("-n" "${report_name} declare" "ef() { return 0; }; declare -Ff ef") |
|
fi |
|
|
|
hyperfine \ |
|
"${hf_params[@]}" \ |
|
"${tests_to_run[@]}" \ |
|
2>> "${BENCHMARK_REPORT_DIR}/hyperfine.error.log" |
|
} |
|
|
|
## Other ways that can be used in bash to check if a function exists |
|
## Discarded as they didn't have an equivalent in zsh |
|
################################################################################################ |
|
# |
|
# As per: https://zsh.sourceforge.io/Doc/Release/Shell-Builtin-Commands.html |
|
# """" |
|
# declare |
|
# Same as typeset. |
|
# """" |
|
## -n "${report_name} typeset" "ef() { return 0; };typeset -fF ef" \ |
|
# |
|
# Compgen is a bash builtin, not a zsh builtin |
|
# so for zsh it requires this to be run first to load the bashcompinit function: |
|
# ``` |
|
# autoload bashcompinit |
|
# bashcompinit |
|
# ``` |
|
# but it still needs to be piped to grep to get the same output as bash |
|
## -n "${report_name} compgen" "ef() { return 0; };compgen -A function | grep -E '^ef$'" |
|
|
|
# Find the path to the brew installed zsh and bash |
|
brew --prefix zsh | read -r BREW_ZSH_PREFIX |
|
brew --prefix bash | read -r BREW_BASH_PREFIX |
|
|
|
# test Success with GNU bash, version 3.2.57(1)-release |
|
[[ -n ${ZSH_TESTS_ONLY+x} ]] || test_with_shell '/bin/bash' |
|
# Test Success with GNU bash, version 5.2.15(1)-release |
|
[[ -n ${ZSH_TESTS_ONLY+x} ]] || test_with_shell "${BREW_BASH_PREFIX}/bin/bash" |
|
# test failure with GNU bash, version 3.2.57(1)-release |
|
[[ -n ${ZSH_TESTS_ONLY+x} ]] || test_with_shell '/bin/bash' "true" |
|
# Test failure with GNU bash, version 5.2.15(1)-release |
|
[[ -n ${ZSH_TESTS_ONLY+x} ]] || test_with_shell "${BREW_BASH_PREFIX}/bin/bash" "true" |
|
|
|
# Test Success with zsh 5.8.1 (x86_64-apple-darwin21.0) |
|
[[ -n ${BASH_TESTS_ONLY+x} ]] || test_with_shell '/bin/zsh' |
|
# Test Success with zsh 5.9 (x86_64-apple-darwin21.3.0) |
|
[[ -n ${BASH_TESTS_ONLY+x} ]] || test_with_shell "${BREW_ZSH_PREFIX}/bin/zsh" |
|
# Test failure with zsh 5.8.1 (x86_64-apple-darwin21.0) |
|
[[ -n ${BASH_TESTS_ONLY+x} ]] || test_with_shell '/bin/zsh' "true" |
|
# Test failure with zsh 5.9 (x86_64-apple-darwin21.3.0) |
|
[[ -n ${BASH_TESTS_ONLY+x} ]] || test_with_shell "${BREW_ZSH_PREFIX}/bin/zsh" "true" |
|
} |
|
|
|
if [[ $1 == 'bash' ]]; then |
|
BASH_TESTS_ONLY=true |
|
elif [[ $1 == 'zsh' ]]; then |
|
ZSH_TESTS_ONLY=true |
|
fi |
|
|
|
main |